about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/about_controller.rb15
-rw-r--r--app/controllers/admin/accounts_controller.rb4
-rw-r--r--app/controllers/admin/settings_controller.rb18
-rw-r--r--app/controllers/api/oembed_controller.rb3
-rw-r--r--app/controllers/api/v1/accounts/credentials_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/follower_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/following_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/relationships_controller.rb11
-rw-r--r--app/controllers/api/v1/accounts/search_controller.rb3
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb5
-rw-r--r--app/controllers/api/v1/accounts_controller.rb38
-rw-r--r--app/controllers/api/v1/apps_controller.rb1
-rw-r--r--app/controllers/api/v1/blocks_controller.rb1
-rw-r--r--app/controllers/api/v1/favourites_controller.rb5
-rw-r--r--app/controllers/api/v1/follow_requests_controller.rb1
-rw-r--r--app/controllers/api/v1/follows_controller.rb2
-rw-r--r--app/controllers/api/v1/instances_controller.rb4
-rw-r--r--app/controllers/api/v1/media_controller.rb1
-rw-r--r--app/controllers/api/v1/mutes_controller.rb1
-rw-r--r--app/controllers/api/v1/notifications_controller.rb7
-rw-r--r--app/controllers/api/v1/reports_controller.rb3
-rw-r--r--app/controllers/api/v1/search_controller.rb6
-rw-r--r--app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/favourites_controller.rb4
-rw-r--r--app/controllers/api/v1/statuses/mutes_controller.rb4
-rw-r--r--app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/reblogs_controller.rb4
-rw-r--r--app/controllers/api/v1/statuses_controller.rb16
-rw-r--r--app/controllers/api/v1/timelines/home_controller.rb6
-rw-r--r--app/controllers/api/v1/timelines/public_controller.rb6
-rw-r--r--app/controllers/api/v1/timelines/tag_controller.rb6
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/authorize_follows_controller.rb2
-rw-r--r--app/controllers/home_controller.rb21
-rw-r--r--app/controllers/settings/preferences_controller.rb2
-rw-r--r--app/helpers/admin/filter_helper.rb12
-rw-r--r--app/helpers/application_helper.rb8
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/javascript/fonts/montserrat/Montserrat-Medium.ttfbin0 -> 192488 bytes
-rw-r--r--app/javascript/images/cloud2.pngbin0 -> 4973 bytes
-rw-r--r--app/javascript/images/cloud3.pngbin0 -> 5860 bytes
-rw-r--r--app/javascript/images/cloud4.pngbin0 -> 5273 bytes
-rw-r--r--app/javascript/images/elephant-fren.pngbin0 -> 40859 bytes
-rw-r--r--app/javascript/images/logo.svg2
-rw-r--r--app/javascript/mastodon/actions/bundles.js25
-rw-r--r--app/javascript/mastodon/actions/notifications.js4
-rw-r--r--app/javascript/mastodon/actions/store.js7
-rw-r--r--app/javascript/mastodon/actions/timelines.js10
-rw-r--r--app/javascript/mastodon/components/column_header.js2
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js19
-rw-r--r--app/javascript/mastodon/components/permalink.js4
-rw-r--r--app/javascript/mastodon/containers/mastodon.js5
-rw-r--r--app/javascript/mastodon/containers/timeline_container.js39
-rw-r--r--app/javascript/mastodon/emoji.js82
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js4
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js3
-rw-r--r--app/javascript/mastodon/features/compose/index.js20
-rw-r--r--app/javascript/mastodon/features/notifications/index.js8
-rw-r--r--app/javascript/mastodon/features/report/containers/status_check_box_container.js4
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js76
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle.js101
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_column_error.js44
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_modal_error.js53
-rw-r--r--app/javascript/mastodon/features/ui/components/column_loading.js19
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js74
-rw-r--r--app/javascript/mastodon/features/ui/components/image_loader.js17
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js19
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_loading.js20
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js55
-rw-r--r--app/javascript/mastodon/features/ui/components/onboarding_modal.js44
-rw-r--r--app/javascript/mastodon/features/ui/components/report_modal.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/tabs_bar.js26
-rw-r--r--app/javascript/mastodon/features/ui/containers/bundle_container.js19
-rw-r--r--app/javascript/mastodon/features/ui/containers/status_list_container.js6
-rw-r--r--app/javascript/mastodon/features/ui/index.js94
-rw-r--r--app/javascript/mastodon/features/ui/util/get_rect_from_entry.js21
-rw-r--r--app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js9
-rw-r--r--app/javascript/mastodon/features/ui/util/react_router_helpers.js57
-rw-r--r--app/javascript/mastodon/locales/ar.json7
-rw-r--r--app/javascript/mastodon/locales/bg.json7
-rw-r--r--app/javascript/mastodon/locales/ca.json7
-rw-r--r--app/javascript/mastodon/locales/de.json7
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json63
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/locales/eo.json7
-rw-r--r--app/javascript/mastodon/locales/es.json7
-rw-r--r--app/javascript/mastodon/locales/fa.json7
-rw-r--r--app/javascript/mastodon/locales/fi.json7
-rw-r--r--app/javascript/mastodon/locales/fr.json63
-rw-r--r--app/javascript/mastodon/locales/he.json7
-rw-r--r--app/javascript/mastodon/locales/hr.json7
-rw-r--r--app/javascript/mastodon/locales/hu.json7
-rw-r--r--app/javascript/mastodon/locales/id.json7
-rw-r--r--app/javascript/mastodon/locales/io.json7
-rw-r--r--app/javascript/mastodon/locales/it.json7
-rw-r--r--app/javascript/mastodon/locales/ja.json7
-rw-r--r--app/javascript/mastodon/locales/ko.json181
-rw-r--r--app/javascript/mastodon/locales/nl.json17
-rw-r--r--app/javascript/mastodon/locales/no.json7
-rw-r--r--app/javascript/mastodon/locales/oc.json15
-rw-r--r--app/javascript/mastodon/locales/pl.json9
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json9
-rw-r--r--app/javascript/mastodon/locales/pt.json7
-rw-r--r--app/javascript/mastodon/locales/ru.json7
-rw-r--r--app/javascript/mastodon/locales/th.json7
-rw-r--r--app/javascript/mastodon/locales/tr.json7
-rw-r--r--app/javascript/mastodon/locales/uk.json7
-rw-r--r--app/javascript/mastodon/locales/whitelist_ko.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_zh-HK.json1
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json9
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json9
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json9
-rw-r--r--app/javascript/mastodon/main.js12
-rw-r--r--app/javascript/mastodon/reducers/accounts.js6
-rw-r--r--app/javascript/mastodon/reducers/accounts_counters.js8
-rw-r--r--app/javascript/mastodon/reducers/alerts.js6
-rw-r--r--app/javascript/mastodon/reducers/cards.js6
-rw-r--r--app/javascript/mastodon/reducers/compose.js31
-rw-r--r--app/javascript/mastodon/reducers/contexts.js18
-rw-r--r--app/javascript/mastodon/reducers/index.js22
-rw-r--r--app/javascript/mastodon/reducers/media_attachments.js4
-rw-r--r--app/javascript/mastodon/reducers/meta.js4
-rw-r--r--app/javascript/mastodon/reducers/notifications.js14
-rw-r--r--app/javascript/mastodon/reducers/relationships.js6
-rw-r--r--app/javascript/mastodon/reducers/reports.js16
-rw-r--r--app/javascript/mastodon/reducers/search.js16
-rw-r--r--app/javascript/mastodon/reducers/settings.js30
-rw-r--r--app/javascript/mastodon/reducers/status_lists.js10
-rw-r--r--app/javascript/mastodon/reducers/statuses.js6
-rw-r--r--app/javascript/mastodon/reducers/timelines.js24
-rw-r--r--app/javascript/mastodon/reducers/user_lists.js32
-rw-r--r--app/javascript/mastodon/selectors/index.js6
-rw-r--r--app/javascript/packs/common.js5
-rw-r--r--app/javascript/packs/public.js18
-rw-r--r--app/javascript/styles/about.scss448
-rw-r--r--app/javascript/styles/basics.scss24
-rw-r--r--app/javascript/styles/boost.scss19
-rw-r--r--app/javascript/styles/components.scss121
-rw-r--r--app/javascript/styles/containers.scss48
-rw-r--r--app/javascript/styles/fonts/montserrat.scss8
-rw-r--r--app/javascript/styles/forms.scss127
-rw-r--r--app/javascript/styles/stream_entries.scss12
-rw-r--r--app/lib/feed_manager.rb4
-rw-r--r--app/lib/inline_rabl_scope.rb17
-rw-r--r--app/lib/inline_renderer.rb36
-rw-r--r--app/lib/user_settings_decorator.rb10
-rw-r--r--app/mailers/admin_mailer.rb2
-rw-r--r--app/mailers/application_mailer.rb1
-rw-r--r--app/mailers/user_mailer.rb1
-rw-r--r--app/models/concerns/remotable.rb23
-rw-r--r--app/models/context.rb5
-rw-r--r--app/models/feed.rb3
-rw-r--r--app/models/form/admin_settings.rb29
-rw-r--r--app/models/media_attachment.rb2
-rw-r--r--app/models/search.rb5
-rw-r--r--app/models/session_activation.rb4
-rw-r--r--app/models/user.rb8
-rw-r--r--app/presenters/account_relationships_presenter.rb15
-rw-r--r--app/presenters/initial_state_presenter.rb5
-rw-r--r--app/presenters/instance_presenter.rb2
-rw-r--r--app/presenters/status_relationships_presenter.rb19
-rw-r--r--app/serializers/initial_state_serializer.rb49
-rw-r--r--app/serializers/oembed_serializer.rb56
-rw-r--r--app/serializers/rest/account_serializer.rb33
-rw-r--r--app/serializers/rest/application_serializer.rb14
-rw-r--r--app/serializers/rest/context_serializer.rb6
-rw-r--r--app/serializers/rest/credential_account_serializer.rb14
-rw-r--r--app/serializers/rest/instance_serializer.rb30
-rw-r--r--app/serializers/rest/media_attachment_serializer.rb24
-rw-r--r--app/serializers/rest/notification_serializer.rb12
-rw-r--r--app/serializers/rest/preview_card_serializer.rb14
-rw-r--r--app/serializers/rest/relationship_serializer.rb30
-rw-r--r--app/serializers/rest/report_serializer.rb5
-rw-r--r--app/serializers/rest/search_serializer.rb12
-rw-r--r--app/serializers/rest/status_serializer.rb93
-rw-r--r--app/services/fan_out_on_write_service.rb2
-rw-r--r--app/services/fetch_atom_service.rb4
-rw-r--r--app/services/fetch_link_card_service.rb10
-rw-r--r--app/services/fetch_remote_account_service.rb3
-rw-r--r--app/services/fetch_remote_resource_service.rb28
-rw-r--r--app/services/fetch_remote_status_service.rb3
-rw-r--r--app/services/notify_service.rb2
-rw-r--r--app/services/precompute_feed_service.rb13
-rw-r--r--app/services/process_feed_service.rb23
-rw-r--r--app/views/about/_features.html.haml25
-rw-r--r--app/views/about/_registration.html.haml20
-rw-r--r--app/views/about/show.html.haml120
-rw-r--r--app/views/about/terms.en.html.haml76
-rw-r--r--app/views/about/terms.html.haml8
-rw-r--r--app/views/about/terms.ja.html.haml76
-rw-r--r--app/views/about/terms.no.html.haml76
-rw-r--r--app/views/admin/accounts/index.html.haml24
-rw-r--r--app/views/admin/reports/show.html.haml4
-rw-r--r--app/views/admin/settings/edit.html.haml84
-rw-r--r--app/views/admin/subscriptions/_subscription.html.haml8
-rw-r--r--app/views/api/oembed/show.json.rabl14
-rw-r--r--app/views/api/v1/accounts/index.rabl2
-rw-r--r--app/views/api/v1/accounts/relationship.rabl9
-rw-r--r--app/views/api/v1/accounts/relationships/index.rabl2
-rw-r--r--app/views/api/v1/accounts/show.rabl12
-rw-r--r--app/views/api/v1/accounts/statuses/index.rabl2
-rw-r--r--app/views/api/v1/apps/create.rabl4
-rw-r--r--app/views/api/v1/apps/show.rabl3
-rw-r--r--app/views/api/v1/blocks/index.rabl2
-rw-r--r--app/views/api/v1/favourites/index.rabl2
-rw-r--r--app/views/api/v1/follow_requests/index.rabl2
-rw-r--r--app/views/api/v1/follows/show.rabl2
-rw-r--r--app/views/api/v1/instances/show.rabl10
-rw-r--r--app/views/api/v1/media/create.rabl7
-rw-r--r--app/views/api/v1/mutes/index.rabl2
-rw-r--r--app/views/api/v1/notifications/index.rabl2
-rw-r--r--app/views/api/v1/notifications/show.rabl11
-rw-r--r--app/views/api/v1/reports/index.rabl2
-rw-r--r--app/views/api/v1/reports/show.rabl2
-rw-r--r--app/views/api/v1/search/index.rabl13
-rw-r--r--app/views/api/v1/statuses/_media.rabl6
-rw-r--r--app/views/api/v1/statuses/_mention.rabl4
-rw-r--r--app/views/api/v1/statuses/_show.rabl29
-rw-r--r--app/views/api/v1/statuses/_tags.rabl2
-rw-r--r--app/views/api/v1/statuses/accounts.rabl2
-rw-r--r--app/views/api/v1/statuses/card.rabl7
-rw-r--r--app/views/api/v1/statuses/context.rabl9
-rw-r--r--app/views/api/v1/statuses/index.rabl2
-rw-r--r--app/views/api/v1/statuses/show.rabl15
-rw-r--r--app/views/api/v1/timelines/show.rabl2
-rw-r--r--app/views/auth/registrations/_sessions.html.haml7
-rw-r--r--app/views/auth/registrations/new.html.haml6
-rw-r--r--app/views/authorize_follows/_card.html.haml3
-rw-r--r--app/views/authorize_follows/success.html.haml16
-rw-r--r--app/views/home/index.html.haml2
-rw-r--r--app/views/home/initial_state.json.rabl37
-rwxr-xr-xapp/views/layouts/application.html.haml10
-rw-r--r--app/views/layouts/auth.html.haml3
-rw-r--r--app/views/layouts/embedded.html.haml2
-rw-r--r--app/views/settings/preferences/show.html.haml3
-rw-r--r--app/views/stream_entries/_content_spoiler.html.haml10
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml6
-rw-r--r--app/views/stream_entries/_simple_status.html.haml3
-rw-r--r--app/views/user_mailer/confirmation_instructions.fr.html.erb4
-rw-r--r--app/views/user_mailer/confirmation_instructions.fr.text.erb6
-rw-r--r--app/views/user_mailer/password_change.pl.html.erb2
-rw-r--r--app/views/user_mailer/password_change.pl.text.erb2
-rw-r--r--app/views/user_mailer/reset_password_instructions.fr.html.erb6
-rw-r--r--app/views/user_mailer/reset_password_instructions.fr.text.erb6
-rw-r--r--app/views/user_mailer/reset_password_instructions.pl.html.erb3
-rw-r--r--app/views/user_mailer/reset_password_instructions.pl.text.erb3
-rw-r--r--app/workers/pubsubhubbub/subscribe_worker.rb15
-rw-r--r--app/workers/push_update_worker.rb2
248 files changed, 3043 insertions, 1368 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 04e7ddacf..47690e81e 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -2,9 +2,12 @@
 
 class AboutController < ApplicationController
   before_action :set_body_classes
-  before_action :set_instance_presenter, only: [:show, :more]
+  before_action :set_instance_presenter, only: [:show, :more, :terms]
 
-  def show; end
+  def show
+    serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
+    @initial_state_json   = serializable_resource.to_json
+  end
 
   def more; end
 
@@ -15,6 +18,7 @@ class AboutController < ApplicationController
   def new_user
     User.new.tap(&:build_account)
   end
+
   helper_method :new_user
 
   def set_instance_presenter
@@ -24,4 +28,11 @@ class AboutController < ApplicationController
   def set_body_classes
     @body_classes = 'about-body'
   end
+
+  def initial_state_params
+    {
+      settings: {},
+      token: current_session&.token,
+    }
+  end
 end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index ef2f8c4c2..7bceee2cd 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -22,8 +22,8 @@ module Admin
     end
 
     def redownload
-      @account.avatar = @account.avatar_remote_url
-      @account.header = @account.header_remote_url
+      @account.reset_avatar!
+      @account.reset_header!
       @account.save!
 
       redirect_to admin_account_path(@account.id)
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index fcd42c79c..5985d6282 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -8,13 +8,21 @@ module Admin
       site_title
       site_description
       site_extended_description
+      site_terms
       open_registrations
       closed_registrations_message
+      open_deletion
+      timeline_preview
+    ).freeze
+
+    BOOLEAN_SETTINGS = %w(
+      open_registrations
+      open_deletion
+      timeline_preview
     ).freeze
-    BOOLEAN_SETTINGS = %w(open_registrations).freeze
 
     def edit
-      @settings = Setting.all_as_records
+      @admin_settings = Form::AdminSettings.new
     end
 
     def update
@@ -23,19 +31,19 @@ module Admin
         setting.update(value: value_for_update(key, value))
       end
 
-      flash[:notice] = 'Success!'
+      flash[:notice] = I18n.t('generic.changes_saved_msg')
       redirect_to edit_admin_settings_path
     end
 
     private
 
     def settings_params
-      params.permit(ADMIN_SETTINGS)
+      params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
     end
 
     def value_for_update(key, value)
       if BOOLEAN_SETTINGS.include?(key)
-        value == 'true'
+        value == '1'
       else
         value
       end
diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb
index 6e3e34d96..f8c87dd16 100644
--- a/app/controllers/api/oembed_controller.rb
+++ b/app/controllers/api/oembed_controller.rb
@@ -5,8 +5,7 @@ class Api::OEmbedController < Api::BaseController
 
   def show
     @stream_entry = find_stream_entry.stream_entry
-    @width = maxwidth_or_default
-    @height = maxheight_or_default
+    render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
   end
 
   private
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 1cf52ff10..073808532 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
 
   def show
     @account = current_account
-    render 'api/v1/accounts/show'
+    render json: @account, serializer: REST::CredentialAccountSerializer
   end
 
   def update
     current_account.update!(account_params)
     @account = current_account
-    render 'api/v1/accounts/show'
+    render json: @account, serializer: REST::CredentialAccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index 81aae56d3..80b0bef40 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
 
   def index
     @accounts = load_accounts
-    render 'api/v1/accounts/index'
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 63c6d54b2..55cffdf37 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
 
   def index
     @accounts = load_accounts
-    render 'api/v1/accounts/index'
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index cb923ab91..a88cf2021 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -8,16 +8,15 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
 
   def index
     @accounts = Account.where(id: account_ids).select('id')
-    @following = Account.following_map(account_ids, current_user.account_id)
-    @followed_by = Account.followed_by_map(account_ids, current_user.account_id)
-    @blocking = Account.blocking_map(account_ids, current_user.account_id)
-    @muting = Account.muting_map(account_ids, current_user.account_id)
-    @requested = Account.requested_map(account_ids, current_user.account_id)
-    @domain_blocking = Account.domain_blocking_map(account_ids, current_user.account_id)
+    render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   private
 
+  def relationships
+    AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
+  end
+
   def account_ids
     @_account_ids ||= Array(params[:id]).map(&:to_i)
   end
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index c4a8f97f2..2a5cac547 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -8,8 +8,7 @@ class Api::V1::Accounts::SearchController < Api::BaseController
 
   def show
     @accounts = account_search
-
-    render 'api/v1/accounts/index'
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 504ed8c07..d9ae5c089 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -9,6 +9,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
 
   def index
     @statuses = load_statuses
+    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
 
   private
@@ -18,9 +19,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
   end
 
   def load_statuses
-    cached_account_statuses.tap do |statuses|
-      set_maps(statuses)
-    end
+    cached_account_statuses
   end
 
   def cached_account_statuses
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 8fc0dd36f..f621aa245 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -8,49 +8,38 @@ class Api::V1::AccountsController < Api::BaseController
 
   respond_to :json
 
-  def show; end
+  def show
+    render json: @account, serializer: REST::AccountSerializer
+  end
 
   def follow
     FollowService.new.call(current_user.account, @account.acct)
-    set_relationship
-    render :relationship
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   def block
     BlockService.new.call(current_user.account, @account)
-
-    @following       = { @account.id => false }
-    @followed_by     = { @account.id => false }
-    @blocking        = { @account.id => true }
-    @requested       = { @account.id => false }
-    @muting          = { @account.id => current_account.muting?(@account.id) }
-    @domain_blocking = { @account.id => current_account.domain_blocking?(@account.domain) }
-
-    render :relationship
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   def mute
     MuteService.new.call(current_user.account, @account)
-    set_relationship
-    render :relationship
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   def unfollow
     UnfollowService.new.call(current_user.account, @account)
-    set_relationship
-    render :relationship
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   def unblock
     UnblockService.new.call(current_user.account, @account)
-    set_relationship
-    render :relationship
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   def unmute
     UnmuteService.new.call(current_user.account, @account)
-    set_relationship
-    render :relationship
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
   private
@@ -59,12 +48,7 @@ class Api::V1::AccountsController < Api::BaseController
     @account = Account.find(params[:id])
   end
 
-  def set_relationship
-    @following       = Account.following_map([@account.id], current_user.account_id)
-    @followed_by     = Account.followed_by_map([@account.id], current_user.account_id)
-    @blocking        = Account.blocking_map([@account.id], current_user.account_id)
-    @muting          = Account.muting_map([@account.id], current_user.account_id)
-    @requested       = Account.requested_map([@account.id], current_user.account_id)
-    @domain_blocking = Account.domain_blocking_map([@account.id], current_user.account_id)
+  def relationships
+    AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
   end
 end
diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb
index 98e908948..44a27b20a 100644
--- a/app/controllers/api/v1/apps_controller.rb
+++ b/app/controllers/api/v1/apps_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::AppsController < Api::BaseController
 
   def create
     @app = Doorkeeper::Application.create!(application_options)
+    render json: @app, serializer: REST::ApplicationSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
index 1702953cf..a412e4341 100644
--- a/app/controllers/api/v1/blocks_controller.rb
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -9,6 +9,7 @@ class Api::V1::BlocksController < Api::BaseController
 
   def index
     @accounts = load_accounts
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index fe0819a3f..92c0a62a9 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -9,14 +9,13 @@ class Api::V1::FavouritesController < Api::BaseController
 
   def index
     @statuses = load_statuses
+    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
 
   private
 
   def load_statuses
-    cached_favourites.tap do |statuses|
-      set_maps(statuses)
-    end
+    cached_favourites
   end
 
   def cached_favourites
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index eed22ef4f..b9f50d784 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -7,6 +7,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
 
   def index
     @accounts = load_accounts
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   def authorize
diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb
index bcdb4e177..e01ae5c01 100644
--- a/app/controllers/api/v1/follows_controller.rb
+++ b/app/controllers/api/v1/follows_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::FollowsController < Api::BaseController
     raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
 
     @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
-    render :show
+    render json: @account, serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb
index ce2181879..1c6971c18 100644
--- a/app/controllers/api/v1/instances_controller.rb
+++ b/app/controllers/api/v1/instances_controller.rb
@@ -3,5 +3,7 @@
 class Api::V1::InstancesController < Api::BaseController
   respond_to :json
 
-  def show; end
+  def show
+    render json: {}, serializer: REST::InstanceSerializer
+  end
 end
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 25a331319..8a1992fca 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -11,6 +11,7 @@ class Api::V1::MediaController < Api::BaseController
 
   def create
     @media = current_account.media_attachments.create!(file: media_params[:file])
+    render json: @media, serializer: REST::MediaAttachmentSerializer
   rescue Paperclip::Errors::NotIdentifiedByImageMagickError
     render json: file_type_error, status: 422
   rescue Paperclip::Error
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
index 2a353df03..0c43cb943 100644
--- a/app/controllers/api/v1/mutes_controller.rb
+++ b/app/controllers/api/v1/mutes_controller.rb
@@ -9,6 +9,7 @@ class Api::V1::MutesController < Api::BaseController
 
   def index
     @accounts = load_accounts
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index a28e99f2f..8910b77e9 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -11,11 +11,12 @@ class Api::V1::NotificationsController < Api::BaseController
 
   def index
     @notifications = load_notifications
-    set_maps_for_notification_target_statuses
+    render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
   end
 
   def show
     @notification = current_account.notifications.find(params[:id])
+    render json: @notification, serializer: REST::NotificationSerializer
   end
 
   def clear
@@ -46,10 +47,6 @@ class Api::V1::NotificationsController < Api::BaseController
     current_account.notifications.browserable(exclude_types)
   end
 
-  def set_maps_for_notification_target_statuses
-    set_maps target_statuses_from_notifications
-  end
-
   def target_statuses_from_notifications
     @notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
   end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 8e7070d07..9592cd4bd 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -9,6 +9,7 @@ class Api::V1::ReportsController < Api::BaseController
 
   def index
     @reports = current_account.reports
+    render json: @reports, each_serializer: REST::ReportSerializer
   end
 
   def create
@@ -20,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
 
     User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
 
-    render :show
+    render json: @report, serializer: REST::ReportSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb
index 8b832148c..bc5b8e5d4 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -3,10 +3,14 @@
 class Api::V1::SearchController < Api::BaseController
   RESULTS_LIMIT = 5
 
+  before_action -> { doorkeeper_authorize! :read }
+  before_action :require_user!
+
   respond_to :json
 
   def index
-    @search = OpenStruct.new(search_results)
+    @search = Search.new(search_results)
+    render json: @search, serializer: REST::SearchSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index e58184939..f95cf9457 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -11,7 +11,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
 
   def index
     @accounts = load_accounts
-    render 'api/v1/statuses/accounts'
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb
index b6fb13cc0..4c4b0c160 100644
--- a/app/controllers/api/v1/statuses/favourites_controller.rb
+++ b/app/controllers/api/v1/statuses/favourites_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
 
   def create
     @status = favourited_status
-    render 'api/v1/statuses/show'
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   def destroy
@@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
 
     UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
 
-    render 'api/v1/statuses/show'
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb
index eab88f2ef..a4bf0acdd 100644
--- a/app/controllers/api/v1/statuses/mutes_controller.rb
+++ b/app/controllers/api/v1/statuses/mutes_controller.rb
@@ -14,14 +14,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController
     current_account.mute_conversation!(@conversation)
     @mutes_map = { @conversation.id => true }
 
-    render 'api/v1/statuses/show'
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   def destroy
     current_account.unmute_conversation!(@conversation)
     @mutes_map = { @conversation.id => false }
 
-    render 'api/v1/statuses/show'
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index 43593d3c5..175217e6e 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -11,7 +11,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
 
   def index
     @accounts = load_accounts
-    render 'api/v1/statuses/accounts'
+    render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index ee9c5b3a6..f7f4b5a5c 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
 
   def create
     @status = ReblogService.new.call(current_user.account, status_for_reblog)
-    render 'api/v1/statuses/show'
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   def destroy
@@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
     authorize status_for_destroy, :unreblog?
     RemovalWorker.perform_async(status_for_destroy.id)
 
-    render 'api/v1/statuses/show'
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   private
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 9aa1cbc4d..9c7124d0f 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -13,6 +13,7 @@ class Api::V1::StatusesController < Api::BaseController
   def show
     cached  = Rails.cache.read(@status.cache_key)
     @status = cached unless cached.nil?
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   def context
@@ -21,15 +22,20 @@ class Api::V1::StatusesController < Api::BaseController
     loaded_ancestors    = cache_collection(ancestors_results, Status)
     loaded_descendants  = cache_collection(descendants_results, Status)
 
-    @context = OpenStruct.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
-    statuses = [@status] + @context[:ancestors] + @context[:descendants]
+    @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
+    statuses = [@status] + @context.ancestors + @context.descendants
 
-    set_maps(statuses)
+    render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
   end
 
   def card
     @card = PreviewCard.find_by(status: @status)
-    render_empty if @card.nil?
+
+    if @card.nil?
+      render_empty
+    else
+      render json: @card, serializer: REST::PreviewCardSerializer
+    end
   end
 
   def create
@@ -43,7 +49,7 @@ class Api::V1::StatusesController < Api::BaseController
                                          application: doorkeeper_token.application,
                                          idempotency: request.headers['Idempotency-Key'])
 
-    render :show
+    render json: @status, serializer: REST::StatusSerializer
   end
 
   def destroy
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index 511d2f65d..3dd27710c 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -9,15 +9,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController
 
   def show
     @statuses = load_statuses
-    render 'api/v1/timelines/show'
+    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
 
   private
 
   def load_statuses
-    cached_home_statuses.tap do |statuses|
-      set_maps(statuses)
-    end
+    cached_home_statuses
   end
 
   def cached_home_statuses
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index 305451cc7..49887778e 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -7,15 +7,13 @@ class Api::V1::Timelines::PublicController < Api::BaseController
 
   def show
     @statuses = load_statuses
-    render 'api/v1/timelines/show'
+    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
 
   private
 
   def load_statuses
-    cached_public_statuses.tap do |statuses|
-      set_maps(statuses)
-    end
+    cached_public_statuses
   end
 
   def cached_public_statuses
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 50afca7c7..08db04a39 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -8,7 +8,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
 
   def show
     @statuses = load_statuses
-    render 'api/v1/timelines/show'
+    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
 
   private
@@ -18,9 +18,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
   end
 
   def load_statuses
-    cached_tagged_statuses.tap do |statuses|
-      set_maps(statuses)
-    end
+    cached_tagged_statuses
   end
 
   def cached_tagged_statuses
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 865fcd125..b3c2db02b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -70,7 +70,7 @@ class ApplicationController < ActionController::Base
   end
 
   def current_session
-    @current_session ||= SessionActivation.find_by(session_id: session['auth_id'])
+    @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
   end
 
   def cache_collection(raw, klass)
diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb
index da4ef022a..dccd1c209 100644
--- a/app/controllers/authorize_follows_controller.rb
+++ b/app/controllers/authorize_follows_controller.rb
@@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController
     if @account.nil?
       render :error
     else
-      redirect_to web_url("accounts/#{@account.id}")
+      render :success
     end
   rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
     render :error
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 6209a3ae9..8a8b9ec76 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -2,13 +2,10 @@
 
 class HomeController < ApplicationController
   before_action :authenticate_user!
+  before_action :set_initial_state_json
 
   def index
-    @body_classes           = 'app-body'
-    @token                  = current_session.token
-    @web_settings           = Web::Setting.find_by(user: current_user)&.data || {}
-    @admin                  = Account.find_local(Setting.site_contact_username)
-    @streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
+    @body_classes = 'app-body'
   end
 
   private
@@ -16,4 +13,18 @@ class HomeController < ApplicationController
   def authenticate_user!
     redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
   end
+
+  def set_initial_state_json
+    serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
+    @initial_state_json   = serializable_resource.to_json
+  end
+
+  def initial_state_params
+    {
+      settings: Web::Setting.find_by(user: current_user)&.data || {},
+      current_account: current_account,
+      token: current_session.token,
+      admin: Account.find_local(Setting.site_contact_username),
+    }
+  end
 end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 71f5a7c04..cac5b0ba8 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -34,9 +34,11 @@ class Settings::PreferencesController < ApplicationController
   def user_settings_params
     params.require(:user).permit(
       :setting_default_privacy,
+      :setting_default_sensitive,
       :setting_boost_modal,
       :setting_delete_modal,
       :setting_auto_play_gif,
+      :setting_system_font_ui,
       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 0dfa30e56..6a57b3d63 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -6,15 +6,21 @@ module Admin::FilterHelper
 
   FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
 
-  def filter_link_to(text, more_params)
-    new_url = filtered_url_for(more_params)
-    link_to text, new_url, class: filter_link_class(new_url)
+  def filter_link_to(text, link_to_params, link_class_params = link_to_params)
+    new_url = filtered_url_for(link_to_params)
+    new_class = filtered_url_for(link_class_params)
+    link_to text, new_url, class: filter_link_class(new_class)
   end
 
   def table_link_to(icon, text, path, options = {})
     link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
   end
 
+  def selected?(more_params)
+    new_url = filtered_url_for(more_params)
+    filter_link_class(new_url) == 'selected' ? true : false
+  end
+
   private
 
   def filter_params(more_params)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 36c37fae0..9f50d8bdb 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -31,7 +31,11 @@ module ApplicationHelper
     Rails.env.production? ? site_title : "#{site_title} (Dev)"
   end
 
-  def fa_icon(icon)
-    content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' '))
+  def fa_icon(icon, attributes = {})
+    class_names = attributes[:class]&.split(' ') || []
+    class_names << 'fa'
+    class_names += icon.split(' ').map { |cl| "fa-#{cl}" }
+
+    content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
   end
 end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 847eff2e7..af950aa63 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -19,6 +19,7 @@ module SettingsHelper
     io: 'Ido',
     it: 'Italiano',
     ja: '日本語',
+    ko: '한국어',
     nl: 'Nederlands',
     no: 'Norsk',
     oc: 'Occitan',
diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf
new file mode 100644
index 000000000..88d70b89c
--- /dev/null
+++ b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf
Binary files differdiff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png
new file mode 100644
index 000000000..f325ca6de
--- /dev/null
+++ b/app/javascript/images/cloud2.png
Binary files differdiff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png
new file mode 100644
index 000000000..ab194d0b8
--- /dev/null
+++ b/app/javascript/images/cloud3.png
Binary files differdiff --git a/app/javascript/images/cloud4.png b/app/javascript/images/cloud4.png
new file mode 100644
index 000000000..98323f5a2
--- /dev/null
+++ b/app/javascript/images/cloud4.png
Binary files differdiff --git a/app/javascript/images/elephant-fren.png b/app/javascript/images/elephant-fren.png
new file mode 100644
index 000000000..3b64edf08
--- /dev/null
+++ b/app/javascript/images/elephant-fren.png
Binary files differdiff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg
index c233db842..16cb3a944 100644
--- a/app/javascript/images/logo.svg
+++ b/app/javascript/images/logo.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#fff"/></svg>
diff --git a/app/javascript/mastodon/actions/bundles.js b/app/javascript/mastodon/actions/bundles.js
new file mode 100644
index 000000000..ecc9c8f7d
--- /dev/null
+++ b/app/javascript/mastodon/actions/bundles.js
@@ -0,0 +1,25 @@
+export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
+export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
+export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
+
+export function fetchBundleRequest(skipLoading) {
+  return {
+    type: BUNDLE_FETCH_REQUEST,
+    skipLoading,
+  };
+}
+
+export function fetchBundleSuccess(skipLoading) {
+  return {
+    type: BUNDLE_FETCH_SUCCESS,
+    skipLoading,
+  };
+}
+
+export function fetchBundleFail(error, skipLoading) {
+  return {
+    type: BUNDLE_FETCH_FAIL,
+    error,
+    skipLoading,
+  };
+}
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index cda636139..c7d248122 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -1,5 +1,5 @@
 import api, { getLinks } from '../api';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 import IntlMessageFormat from 'intl-messageformat';
 import { fetchRelationships } from './accounts';
 import { defineMessages } from 'react-intl';
@@ -124,7 +124,7 @@ export function refreshNotificationsFail(error, skipLoading) {
 
 export function expandNotifications() {
   return (dispatch, getState) => {
-    const items  = getState().getIn(['notifications', 'items'], Immutable.List());
+    const items  = getState().getIn(['notifications', 'items'], ImmutableList());
 
     if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
       return;
diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index 601cea001..0597d265e 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -1,10 +1,11 @@
-import Immutable from 'immutable';
+import { Iterable, fromJS } from 'immutable';
 
 export const STORE_HYDRATE = 'STORE_HYDRATE';
+export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
 
 const convertState = rawState =>
-  Immutable.fromJS(rawState, (k, v) =>
-    Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
+  fromJS(rawState, (k, v) =>
+    Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
       Number.isNaN(x * 1) ? x : x * 1));
 
 export function hydrateStore(rawState) {
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index cb4410eba..dd14cb1cd 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -1,5 +1,5 @@
 import api, { getLinks } from '../api';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
 export const TIMELINE_DELETE  = 'TIMELINE_DELETE';
@@ -66,13 +66,13 @@ export function refreshTimelineRequest(timeline, skipLoading) {
 
 export function refreshTimeline(timelineId, path, params = {}) {
   return function (dispatch, getState) {
-    const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
+    const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
 
     if (timeline.get('isLoading') || timeline.get('online')) {
       return;
     }
 
-    const ids      = timeline.get('items', Immutable.List());
+    const ids      = timeline.get('items', ImmutableList());
     const newestId = ids.size > 0 ? ids.first() : null;
 
     let skipLoading = timeline.get('loaded');
@@ -111,8 +111,8 @@ export function refreshTimelineFail(timeline, error, skipLoading) {
 
 export function expandTimeline(timelineId, path, params = {}) {
   return (dispatch, getState) => {
-    const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
-    const ids      = timeline.get('items', Immutable.List());
+    const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
+    const ids      = timeline.get('items', ImmutableList());
 
     if (timeline.get('isLoading') || ids.size === 0) {
       return;
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index 027d01767..e9f041be6 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -10,7 +10,7 @@ export default class ColumnHeader extends React.PureComponent {
   };
 
   static propTypes = {
-    title: PropTypes.string.isRequired,
+    title: PropTypes.node.isRequired,
     icon: PropTypes.string.isRequired,
     active: PropTypes.bool,
     multiColumn: PropTypes.bool,
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 12e1b44fa..98323b069 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent {
     size: PropTypes.number.isRequired,
     direction: PropTypes.string,
     ariaLabel: PropTypes.string,
+    disabled: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent {
   }
 
   render () {
-    const { icon, items, size, direction, ariaLabel } = this.props;
-    const { expanded } = this.state;
+    const { icon, items, size, direction, ariaLabel, disabled } = this.props;
+    const { expanded }   = this.state;
     const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
+    const iconStyle      = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
+    const iconClassname  = `fa fa-fw fa-${icon} dropdown__icon`;
+
+    if (disabled) {
+      return (
+        <div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}>
+          <i className={iconClassname} aria-hidden />
+        </div>
+      );
+    }
 
     const dropdownItems = expanded && (
       <ul className='dropdown__content-list'>
@@ -80,8 +91,8 @@ export default class DropdownMenu extends React.PureComponent {
 
     return (
       <Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
-        <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
-          <i className={`fa fa-fw fa-${icon} dropdown__icon`}  aria-hidden />
+        <DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
+          <i className={iconClassname} aria-hidden />
         </DropdownTrigger>
 
         <DropdownContent className={directionClass}>
diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js
index 0b7d0a65a..d726d37a2 100644
--- a/app/javascript/mastodon/components/permalink.js
+++ b/app/javascript/mastodon/components/permalink.js
@@ -15,7 +15,7 @@ export default class Permalink extends React.PureComponent {
   };
 
   handleClick = (e) => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
       this.context.router.history.push(this.props.to);
     }
@@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
     const { href, children, className, ...other } = this.props;
 
     return (
-      <a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
+      <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
         {children}
       </a>
     );
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 398f7d243..8287375c4 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -22,14 +22,15 @@ import { getLocale } from '../locales';
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
-const store = configureStore();
+export const store = configureStore();
 const initialState = JSON.parse(document.getElementById('initial-state').textContent);
 try {
   initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
 } catch (e) {
   initialState.local_settings = {};
 }
-store.dispatch(hydrateStore(initialState));
+const hydrateAction = hydrateStore(initialState);
+store.dispatch(hydrateAction);
 
 export default class Mastodon extends React.PureComponent {
 
diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js
new file mode 100644
index 000000000..6b545ef09
--- /dev/null
+++ b/app/javascript/mastodon/containers/timeline_container.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import PropTypes from 'prop-types';
+import configureStore from '../store/configureStore';
+import { hydrateStore } from '../actions/store';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import PublicTimeline from '../features/standalone/public_timeline';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+const store = configureStore();
+const initialStateContainer = document.getElementById('initial-state');
+
+if (initialStateContainer !== null) {
+  const initialState = JSON.parse(initialStateContainer.textContent);
+  store.dispatch(hydrateStore(initialState));
+}
+
+export default class TimelineContainer extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+  };
+
+  render () {
+    const { locale } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        <Provider store={store}>
+          <PublicTimeline />
+        </Provider>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index 01d01fb72..7043d5f3a 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -1,35 +1,55 @@
 import emojione from 'emojione';
-
-const toImage = str => shortnameToImage(unicodeToImage(str));
-
-const unicodeToImage = str => {
-  const mappedUnicode = emojione.mapUnicodeToShort();
-
-  return str.replace(emojione.regUnicode, unicodeChar => {
-    if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) {
-      return unicodeChar;
+import Trie from 'substring-trie';
+
+const mappedUnicode = emojione.mapUnicodeToShort();
+const trie = new Trie(Object.keys(emojione.jsEscapeMap));
+
+function emojify(str) {
+  // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
+  // and replacing valid shortnames like :smile: and :wink: as well as unicode strings
+  // that _aren't_ within tags with an <img> version.
+  // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster.
+  let i = -1;
+  let insideTag = false;
+  let insideShortname = false;
+  let shortnameStartIndex = -1;
+  let match;
+  while (++i < str.length) {
+    const char = str.charAt(i);
+    if (insideShortname && char === ':') {
+      const shortname = str.substring(shortnameStartIndex, i + 1);
+      if (shortname in emojione.emojioneList) {
+        const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
+        const alt = emojione.convert(unicode.toUpperCase());
+        const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
+        str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
+        i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
+      } else {
+        i--; // stray colon, try again
+      }
+      insideShortname = false;
+    } else if (insideTag && char === '>') {
+      insideTag = false;
+    } else if (char === '<') {
+      insideTag = true;
+      insideShortname = false;
+    } else if (!insideTag && char === ':') {
+      insideShortname = true;
+      shortnameStartIndex = i;
+    } else if (!insideTag && (match = trie.search(str.substring(i)))) {
+      const unicodeStr = match;
+      if (unicodeStr in emojione.jsEscapeMap) {
+        const unicode  = emojione.jsEscapeMap[unicodeStr];
+        const short    = mappedUnicode[unicode];
+        const filename = emojione.emojioneList[short].fname;
+        const alt      = emojione.convert(unicode.toUpperCase());
+        const replacement =  `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
+        str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
+        i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
+      }
     }
-
-    const unicode  = emojione.jsEscapeMap[unicodeChar];
-    const short    = mappedUnicode[unicode];
-    const filename = emojione.emojioneList[short].fname;
-    const alt      = emojione.convert(unicode.toUpperCase());
-
-    return `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
-  });
-};
-
-const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => {
-  if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) {
-    return shortname;
   }
+  return str;
+}
 
-  const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
-  const alt     = emojione.convert(unicode.toUpperCase());
-
-  return `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
-});
-
-export default function emojify(text) {
-  return toImage(text);
-};
+export default emojify;
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 955d0000e..3c8b63114 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -9,11 +9,11 @@ import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
 import HeaderContainer from './containers/header_container';
 import ColumnBackButton from '../../components/column_back_button';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], Immutable.List()),
+  statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()),
   isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']),
   hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']),
   me: state.getIn(['meta', 'me']),
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index c83dbb63e..83c66a5d5 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -2,6 +2,7 @@ import React from 'react';
 import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
+import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
     this.setState({ active: true });
     if (!EmojiPicker) {
       this.setState({ loading: true });
-      import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => {
+      EmojiPickerAsync().then(TheEmojiPicker => {
         EmojiPicker = TheEmojiPicker.default;
         this.setState({ loading: false });
       }).catch(() => {
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 3ec205f2c..69bead689 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -2,6 +2,7 @@ import React from 'react';
 import ComposeFormContainer from './containers/compose_form_container';
 import NavigationContainer from './containers/navigation_container';
 import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
 import { mountCompose, unmountCompose } from '../../actions/compose';
 import { openModal } from '../../actions/modal';
@@ -15,6 +16,8 @@ import SearchResultsContainer from './containers/search_results_container';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+  home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
+  notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
@@ -22,6 +25,7 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = state => ({
+  columns: state.getIn(['settings', 'columns']),
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
 });
 
@@ -31,6 +35,7 @@ export default class Compose extends React.PureComponent {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
+    columns: ImmutablePropTypes.list.isRequired,
     multiColumn: PropTypes.bool,
     showSearch: PropTypes.bool,
     intl: PropTypes.object.isRequired,
@@ -60,11 +65,22 @@ export default class Compose extends React.PureComponent {
     let header = '';
 
     if (multiColumn) {
+      const { columns } = this.props;
       header = (
         <div className='drawer__header'>
           <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
-          <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
-          <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
+          {!columns.some(column => column.get('id') === 'HOME') && (
+            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link>
+          )}
+          {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
+            <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link>
+          )}
+          {!columns.some(column => column.get('id') === 'COMMUNITY') && (
+            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
+          )}
+          {!columns.some(column => column.get('id') === 'PUBLIC') && (
+            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
+          )}
           <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a>
           <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
         </div>
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index d4e1555b2..fb8f0d3cc 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -11,7 +11,7 @@ import { ScrollContainer } from 'react-router-scroll';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 import LoadMore from '../../components/load_more';
 import { debounce } from 'lodash';
 
@@ -20,7 +20,7 @@ const messages = defineMessages({
 });
 
 const getNotifications = createSelector([
-  state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
+  state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
   state => state.getIn(['notifications', 'items']),
 ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
 
@@ -122,7 +122,7 @@ export default class Notifications extends React.PureComponent {
     let unread         = '';
     let scrollContainer = '';
 
-    if (!isLoading && notifications.size > 0 && hasMore) {
+    if (!isLoading && hasMore) {
       loadMore = <LoadMore onClick={this.handleLoadMore} />;
     }
 
@@ -132,7 +132,7 @@ export default class Notifications extends React.PureComponent {
 
     if (isLoading && this.scrollableArea) {
       scrollableArea = this.scrollableArea;
-    } else if (notifications.size > 0) {
+    } else if (notifications.size > 0 || hasMore) {
       scrollableArea = (
         <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
           {unread}
diff --git a/app/javascript/mastodon/features/report/containers/status_check_box_container.js b/app/javascript/mastodon/features/report/containers/status_check_box_container.js
index 8997718a2..48cd0319b 100644
--- a/app/javascript/mastodon/features/report/containers/status_check_box_container.js
+++ b/app/javascript/mastodon/features/report/containers/status_check_box_container.js
@@ -1,11 +1,11 @@
 import { connect } from 'react-redux';
 import StatusCheckBox from '../components/status_check_box';
 import { toggleStatusReport } from '../../../actions/reports';
-import Immutable from 'immutable';
+import { Set as ImmutableSet } from 'immutable';
 
 const mapStateToProps = (state, { id }) => ({
   status: state.getIn(['statuses', id]),
-  checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id),
+  checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
 });
 
 const mapDispatchToProps = (dispatch, { id }) => ({
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
new file mode 100644
index 000000000..de4b5320a
--- /dev/null
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import StatusListContainer from '../../ui/containers/status_list_container';
+import {
+  refreshPublicTimeline,
+  expandPublicTimeline,
+} from '../../../actions/timelines';
+import Column from '../../../components/column';
+import ColumnHeader from '../../../components/column_header';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
+});
+
+@connect()
+@injectIntl
+export default class PublicTimeline extends React.PureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  }
+
+  setRef = c => {
+    this.column = c;
+  }
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+
+    dispatch(refreshPublicTimeline());
+
+    this.polling = setInterval(() => {
+      dispatch(refreshPublicTimeline());
+    }, 3000);
+  }
+
+  componentWillUnmount () {
+    if (typeof this.polling !== 'undefined') {
+      clearInterval(this.polling);
+      this.polling = null;
+    }
+  }
+
+  handleLoadMore = () => {
+    this.props.dispatch(expandPublicTimeline());
+  }
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Column ref={this.setRef}>
+        <ColumnHeader
+          icon='globe'
+          title={intl.formatMessage(messages.title)}
+          onClick={this.handleHeaderClick}
+        />
+
+        <StatusListContainer
+          timelineId='public'
+          loadMore={this.handleLoadMore}
+          scrollKey='standalone_public_timeline'
+          trackScroll={false}
+        />
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js
new file mode 100644
index 000000000..72798f690
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/bundle.js
@@ -0,0 +1,101 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const emptyComponent = () => null;
+const noop = () => { };
+
+class Bundle extends React.Component {
+
+  static propTypes = {
+    fetchComponent: PropTypes.func.isRequired,
+    loading: PropTypes.func,
+    error: PropTypes.func,
+    children: PropTypes.func.isRequired,
+    renderDelay: PropTypes.number,
+    onFetch: PropTypes.func,
+    onFetchSuccess: PropTypes.func,
+    onFetchFail: PropTypes.func,
+  }
+
+  static defaultProps = {
+    loading: emptyComponent,
+    error: emptyComponent,
+    renderDelay: 0,
+    onFetch: noop,
+    onFetchSuccess: noop,
+    onFetchFail: noop,
+  }
+
+  static cache = {}
+
+  state = {
+    mod: undefined,
+    forceRender: false,
+  }
+
+  componentWillMount() {
+    this.load(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.fetchComponent !== this.props.fetchComponent) {
+      this.load(nextProps);
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.timeout) {
+      clearTimeout(this.timeout);
+    }
+  }
+
+  load = (props) => {
+    const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
+
+    this.setState({ mod: undefined });
+    onFetch();
+
+    if (renderDelay !== 0) {
+      this.timestamp = new Date();
+      this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay);
+    }
+
+    if (Bundle.cache[fetchComponent.name]) {
+      const mod = Bundle.cache[fetchComponent.name];
+
+      this.setState({ mod: mod.default });
+      onFetchSuccess();
+      return Promise.resolve();
+    }
+
+    return fetchComponent()
+      .then((mod) => {
+        Bundle.cache[fetchComponent.name] = mod;
+        this.setState({ mod: mod.default });
+        onFetchSuccess();
+      })
+      .catch((error) => {
+        this.setState({ mod: null });
+        onFetchFail(error);
+      });
+  }
+
+  render() {
+    const { loading: Loading, error: Error, children, renderDelay } = this.props;
+    const { mod, forceRender } = this.state;
+    const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay;
+
+    if (mod === undefined) {
+      return (elapsed >= renderDelay || forceRender) ? <Loading /> : null;
+    }
+
+    if (mod === null) {
+      return <Error onRetry={this.load} />;
+    }
+
+    return children(mod);
+  }
+
+}
+
+export default Bundle;
diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
new file mode 100644
index 000000000..cd124746a
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import Column from './column';
+import ColumnHeader from './column_header';
+import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
+  body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' },
+  retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
+});
+
+class BundleColumnError extends React.Component {
+
+  static propTypes = {
+    onRetry: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  }
+
+  handleRetry = () => {
+    this.props.onRetry();
+  }
+
+  render () {
+    const { intl: { formatMessage } } = this.props;
+
+    return (
+      <Column>
+        <ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} />
+        <ColumnBackButtonSlim />
+        <div className='error-column'>
+          <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
+          {formatMessage(messages.body)}
+        </div>
+      </Column>
+    );
+  }
+
+}
+
+export default injectIntl(BundleColumnError);
diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
new file mode 100644
index 000000000..928bfe1f7
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
+  retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
+  close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
+});
+
+class BundleModalError extends React.Component {
+
+  static propTypes = {
+    onRetry: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  }
+
+  handleRetry = () => {
+    this.props.onRetry();
+  }
+
+  render () {
+    const { onClose, intl: { formatMessage } } = this.props;
+
+    // Keep the markup in sync with <ModalLoading />
+    // (make sure they have the same dimensions)
+    return (
+      <div className='modal-root__modal error-modal'>
+        <div className='error-modal__body'>
+          <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
+          {formatMessage(messages.error)}
+        </div>
+
+        <div className='error-modal__footer'>
+          <div>
+            <button
+              onClick={onClose}
+              className='error-modal__nav onboarding-modal__skip'
+            >
+              {formatMessage(messages.close)}
+            </button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(BundleModalError);
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js
new file mode 100644
index 000000000..7ecfaf77a
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/column_loading.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Column from '../../../components/column';
+import ColumnHeader from '../../../components/column_header';
+
+const ColumnLoading = ({ title = '', icon = ' ' }) => (
+  <Column>
+    <ColumnHeader icon={icon} title={title} multiColumn={false} />
+    <div className='scrollable' />
+  </Column>
+);
+
+ColumnLoading.propTypes = {
+  title: PropTypes.node,
+  icon: PropTypes.string,
+};
+
+export default ColumnLoading;
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 3c3e9425d..cbc185a7d 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -2,14 +2,14 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import ReactSwipeable from 'react-swipeable';
-import HomeTimeline from '../../home_timeline';
-import Notifications from '../../notifications';
-import PublicTimeline from '../../public_timeline';
-import CommunityTimeline from '../../community_timeline';
-import HashtagTimeline from '../../hashtag_timeline';
-import Compose from '../../compose';
-import { getPreviousLink, getNextLink } from './tabs_bar';
+
+import ReactSwipeableViews from 'react-swipeable-views';
+import { links, getIndex, getLink } from './tabs_bar';
+
+import BundleContainer from '../containers/bundle_container';
+import ColumnLoading from './column_loading';
+import BundleColumnError from './bundle_column_error';
+import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components';
 
 const componentMap = {
   'COMPOSE': Compose,
@@ -32,39 +32,61 @@ export default class ColumnsArea extends ImmutablePureComponent {
     children: PropTypes.node,
   };
 
-  handleRightSwipe = () => {
-    const previousLink = getPreviousLink(this.context.router.history.location.pathname);
+  handleSwipe = (index) => {
+    window.requestAnimationFrame(() => {
+      window.requestAnimationFrame(() => {
+        this.context.router.history.push(getLink(index));
+      });
+    });
+  }
 
-    if (previousLink) {
-      this.context.router.history.push(previousLink);
-    }
+  renderView = (link, index) => {
+    const columnIndex = getIndex(this.context.router.history.location.pathname);
+    const title = link.props.children[1] && React.cloneElement(link.props.children[1]);
+    const icon = (link.props.children[0] || link.props.children).props.className.split(' ')[2].split('-')[1];
+
+    const view = (index === columnIndex) ?
+      React.cloneElement(this.props.children) :
+      <ColumnLoading title={title} icon={icon} />;
+
+    return (
+      <div className='columns-area' key={index}>
+        {view}
+      </div>
+    );
   }
 
-  handleLeftSwipe = () => {
-    const previousLink = getNextLink(this.context.router.history.location.pathname);
+  renderLoading = () => {
+    return <ColumnLoading />;
+  }
 
-    if (previousLink) {
-      this.context.router.history.push(previousLink);
-    }
-  };
+  renderError = (props) => {
+    return <BundleColumnError {...props} />;
+  }
 
   render () {
     const { columns, children, singleColumn } = this.props;
 
+    const columnIndex = getIndex(this.context.router.history.location.pathname);
+
     if (singleColumn) {
-      return (
-        <ReactSwipeable onSwipedLeft={this.handleLeftSwipe} onSwipedRight={this.handleRightSwipe} className='columns-area'>
-          {children}
-        </ReactSwipeable>
-      );
+      return columnIndex !== -1 ? (
+        <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} animateTransitions={false} style={{ height: '100%' }}>
+          {links.map(this.renderView)}
+        </ReactSwipeableViews>
+      ) : <div className='columns-area'>{children}</div>;
     }
 
     return (
       <div className='columns-area'>
         {columns.map(column => {
-          const SpecificComponent = componentMap[column.get('id')];
           const params = column.get('params', null) === null ? null : column.get('params').toJS();
-          return <SpecificComponent key={column.get('uuid')} columnId={column.get('uuid')} params={params} multiColumn />;
+
+          return (
+            <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading} error={this.renderError}>
+              {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />}
+            </BundleContainer>
+          );
         })}
 
         {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js
index 52c3a898b..aad594380 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.js
@@ -8,12 +8,14 @@ export default class ImageLoader extends React.PureComponent {
     alt: PropTypes.string,
     src: PropTypes.string.isRequired,
     previewSrc: PropTypes.string.isRequired,
-    width: PropTypes.number.isRequired,
-    height: PropTypes.number.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
   }
 
   static defaultProps = {
     alt: '',
+    width: null,
+    height: null,
   };
 
   state = {
@@ -46,8 +48,8 @@ export default class ImageLoader extends React.PureComponent {
     this.setState({ loading: true, error: false });
     Promise.all([
       this.loadPreviewCanvas(props),
-      this.loadOriginalImage(props),
-    ])
+      this.hasSize() && this.loadOriginalImage(props),
+    ].filter(Boolean))
       .then(() => {
         this.setState({ loading: false, error: false });
         this.clearPreviewCanvas();
@@ -106,6 +108,11 @@ export default class ImageLoader extends React.PureComponent {
     this.removers = [];
   }
 
+  hasSize () {
+    const { width, height } = this.props;
+    return typeof width === 'number' && typeof height === 'number';
+  }
+
   setCanvasRef = c => {
     this.canvas = c;
   }
@@ -116,6 +123,7 @@ export default class ImageLoader extends React.PureComponent {
 
     const className = classNames('image-loader', {
       'image-loader--loading': loading,
+      'image-loader--amorphous': !this.hasSize(),
     });
 
     return (
@@ -125,6 +133,7 @@ export default class ImageLoader extends React.PureComponent {
           width={width}
           height={height}
           ref={this.setCanvasRef}
+          style={{ opacity: loading ? 1 : 0 }}
         />
 
         {!loading && (
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 8bb81ca01..d869fffa6 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import ReactSwipeable from 'react-swipeable';
+import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ExtendedVideoPlayer from '../../../components/extended_video_player';
@@ -26,12 +26,16 @@ export default class MediaModal extends ImmutablePureComponent {
     index: null,
   };
 
+  handleSwipe = (index) => {
+    this.setState({ index: (index) % this.props.media.size });
+  }
+
   handleNextClick = () => {
     this.setState({ index: (this.getIndex() + 1) % this.props.media.size });
   }
 
   handlePrevClick = () => {
-    this.setState({ index: (this.getIndex() - 1) % this.props.media.size });
+    this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size });
   }
 
   handleKeyUp = (e) => {
@@ -74,7 +78,12 @@ export default class MediaModal extends ImmutablePureComponent {
     }
 
     if (attachment.get('type') === 'image') {
-      content = <ImageLoader previewSrc={attachment.get('preview_url')} src={url} width={attachment.getIn(['meta', 'original', 'width'])} height={attachment.getIn(['meta', 'original', 'height'])} />;
+      content = media.map((image) => {
+        const width  = image.getIn(['meta', 'original', 'width']) || null;
+        const height = image.getIn(['meta', 'original', 'height']) || null;
+
+        return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} key={image.get('preview_url')} />;
+      }).toArray();
     } else if (attachment.get('type') === 'gifv') {
       content = <ExtendedVideoPlayer src={url} muted controls={false} />;
     }
@@ -85,9 +94,9 @@ export default class MediaModal extends ImmutablePureComponent {
 
         <div className='media-modal__content'>
           <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
-          <ReactSwipeable onSwipedRight={this.handlePrevClick} onSwipedLeft={this.handleNextClick}>
+          <ReactSwipeableViews onChangeIndex={this.handleSwipe} index={index} animateHeight>
             {content}
-          </ReactSwipeable>
+          </ReactSwipeableViews>
         </div>
 
         {rightNav}
diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.js
new file mode 100644
index 000000000..f403ca4c9
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/modal_loading.js
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import LoadingIndicator from '../../../components/loading_indicator';
+
+// Keep the markup in sync with <BundleModalError />
+// (make sure they have the same dimensions)
+const ModalLoading = () => (
+  <div className='modal-root__modal error-modal'>
+    <div className='error-modal__body'>
+      <LoadingIndicator />
+    </div>
+    <div className='error-modal__footer'>
+      <div>
+        <button className='error-modal__nav onboarding-modal__skip' />
+      </div>
+    </div>
+  </div>
+);
+
+export default ModalLoading;
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 3777c1bf6..de4f44ce6 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -1,14 +1,19 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import MediaModal from './media_modal';
-import OnboardingModal from './onboarding_modal';
-import VideoModal from './video_modal';
-import BoostModal from './boost_modal';
-import ConfirmationModal from './confirmation_modal';
-import ReportModal from './report_modal';
-import SettingsContainer from '../../../../glitch/containers/settings';
 import TransitionMotion from 'react-motion/lib/TransitionMotion';
 import spring from 'react-motion/lib/spring';
+import BundleContainer from '../containers/bundle_container';
+import BundleModalError from './bundle_modal_error';
+import ModalLoading from './modal_loading';
+import {
+  MediaModal,
+  OnboardingModal,
+  VideoModal,
+  BoostModal,
+  ConfirmationModal,
+  ReportModal,
+  SettingsModal,
+} from '../../../features/ui/util/async-components';
 
 const MODAL_COMPONENTS = {
   'MEDIA': MediaModal,
@@ -17,7 +22,7 @@ const MODAL_COMPONENTS = {
   'BOOST': BoostModal,
   'CONFIRM': ConfirmationModal,
   'REPORT': ReportModal,
-  'SETTINGS': SettingsContainer,
+  'SETTINGS': SettingsModal,
 };
 
 export default class ModalRoot extends React.PureComponent {
@@ -51,6 +56,22 @@ export default class ModalRoot extends React.PureComponent {
     return { opacity: spring(0), scale: spring(0.98) };
   }
 
+  renderModal = (SpecificComponent) => {
+    const { props, onClose } = this.props;
+
+    return <SpecificComponent {...props} onClose={onClose} />;
+  }
+
+  renderLoading = () => {
+    return <ModalLoading />;
+  }
+
+  renderError = (props) => {
+    const { onClose } = this.props;
+
+    return <BundleModalError {...props} onClose={onClose} />;
+  }
+
   render () {
     const { type, props, onClose } = this.props;
     const visible = !!type;
@@ -72,18 +93,14 @@ export default class ModalRoot extends React.PureComponent {
       >
         {interpolatedStyles =>
           <div className='modal-root'>
-            {interpolatedStyles.map(({ key, data: { type, props }, style }) => {
-              const SpecificComponent = MODAL_COMPONENTS[type];
-
-              return (
-                <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
-                  <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
-                  <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
-                    <SpecificComponent {...props} onClose={onClose} />
-                  </div>
+            {interpolatedStyles.map(({ key, data: { type }, style }) => (
+              <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
+                <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
+                <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
+                  <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>{this.renderModal}</BundleContainer>
                 </div>
-              );
-            })}
+              </div>
+            ))}
           </div>
         }
       </TransitionMotion>
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
index dab5e47ea..1b1cb00da 100644
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -3,16 +3,14 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ReactSwipeable from 'react-swipeable';
+import ReactSwipeableViews from 'react-swipeable-views';
 import classNames from 'classnames';
 import Permalink from '../../../components/permalink';
-import TransitionMotion from 'react-motion/lib/TransitionMotion';
-import spring from 'react-motion/lib/spring';
 import ComposeForm from '../../compose/components/compose_form';
 import Search from '../../compose/components/search';
 import NavigationBar from '../../compose/components/navigation_bar';
 import ColumnHeader from './column_header';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 
 const noop = () => { };
 
@@ -50,7 +48,7 @@ const PageTwo = ({ me }) => (
       </div>
       <ComposeForm
         text='Awoo! #introductions'
-        suggestions={Immutable.List()}
+        suggestions={ImmutableList()}
         mentionedDomains={[]}
         spoiler={false}
         onChange={noop}
@@ -227,6 +225,10 @@ export default class OnboardingModal extends React.PureComponent {
     }));
   }
 
+  handleSwipe = (index) => {
+    this.setState({ currentIndex: index });
+  }
+
   handleKeyUp = ({ key }) => {
     switch (key) {
     case 'ArrowLeft':
@@ -263,30 +265,18 @@ export default class OnboardingModal extends React.PureComponent {
       </button>
     );
 
-    const styles = pages.map((data, i) => ({
-      key: `page-${i}`,
-      data,
-      style: {
-        opacity: spring(i === currentIndex ? 1 : 0),
-      },
-    }));
-
     return (
       <div className='modal-root__modal onboarding-modal'>
-        <TransitionMotion styles={styles}>
-          {interpolatedStyles => (
-            <ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'>
-              {interpolatedStyles.map(({ key, data, style }, i) => {
-                const className = classNames('onboarding-modal__page__wrapper', {
-                  'onboarding-modal__page__wrapper--active': i === currentIndex,
-                });
-                return (
-                  <div key={key} style={style} className={className}>{data}</div>
-                );
-              })}
-            </ReactSwipeable>
-          )}
-        </TransitionMotion>
+        <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'>
+          {pages.map((page, i) => {
+            const className = classNames('onboarding-modal__page__wrapper', {
+              'onboarding-modal__page__wrapper--active': i === currentIndex,
+            });
+            return (
+              <div key={i} className={className}>{page}</div>
+            );
+          })}
+        </ReactSwipeableViews>
 
         <div className='onboarding-modal__paginator'>
           <div>
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js
index c989d2c9b..b5dfa422e 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.js
@@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { makeGetAccount } from '../../../selectors';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 import StatusCheckBox from '../../report/containers/status_check_box_container';
-import Immutable from 'immutable';
+import { OrderedSet } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Button from '../../../components/button';
 
@@ -26,7 +26,7 @@ const makeMapStateToProps = () => {
       isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
       account: getAccount(state, accountId),
       comment: state.getIn(['reports', 'new', 'comment']),
-      statusIds: Immutable.OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
+      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
     };
   };
 
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index c2e6c88b5..b4153ff45 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -2,7 +2,7 @@ import React from 'react';
 import NavLink from 'react-router-dom/NavLink';
 import { FormattedMessage } from 'react-intl';
 
-const links = [
+export const links = [
   <NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
   <NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
   <NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
@@ -13,25 +13,13 @@ const links = [
   <NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></NavLink>,
 ];
 
-export function getPreviousLink (path) {
-  const index = links.findIndex(link => link.props.to === path);
-
-  if (index > 0) {
-    return links[index - 1].props.to;
-  }
-
-  return null;
-};
-
-export function getNextLink (path) {
-  const index = links.findIndex(link => link.props.to === path);
-
-  if (index !== -1 && index < links.length - 1) {
-    return links[index + 1].props.to;
-  }
+export function getIndex (path) {
+  return links.findIndex(link => link.props.to === path);
+}
 
-  return null;
-};
+export function getLink (index) {
+  return links[index].props.to;
+}
 
 export default class TabsBar extends React.Component {
 
diff --git a/app/javascript/mastodon/features/ui/containers/bundle_container.js b/app/javascript/mastodon/features/ui/containers/bundle_container.js
new file mode 100644
index 000000000..7e3f0c3a6
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/containers/bundle_container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import Bundle from '../components/bundle';
+
+import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
+
+const mapDispatchToProps = dispatch => ({
+  onFetch () {
+    dispatch(fetchBundleRequest());
+  },
+  onFetchSuccess () {
+    dispatch(fetchBundleSuccess());
+  },
+  onFetchFail (error) {
+    dispatch(fetchBundleFail(error));
+  },
+});
+
+export default connect(null, mapDispatchToProps)(Bundle);
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index 45ad6209b..1b2e1056a 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -1,13 +1,13 @@
 import { connect } from 'react-redux';
 import StatusList from '../../../components/status_list';
 import { scrollTopTimeline } from '../../../actions/timelines';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { debounce } from 'lodash';
 
 const makeGetStatusIds = () => createSelector([
-  (state, { type }) => state.getIn(['settings', type], Immutable.Map()),
-  (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
+  (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
+  (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
   (state)           => state.get('statuses'),
   (state)           => state.getIn(['meta', 'me']),
 ], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 824963a53..5a0398eb4 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -1,6 +1,5 @@
 import React from 'react';
-import Switch from 'react-router-dom/Switch';
-import Route from 'react-router-dom/Route';
+import classNames from 'classnames';
 import Redirect from 'react-router-dom/Redirect';
 import NotificationsContainer from './containers/notifications_container';
 import PropTypes from 'prop-types';
@@ -13,66 +12,37 @@ import { debounce } from 'lodash';
 import { uploadCompose } from '../../actions/compose';
 import { refreshHomeTimeline } from '../../actions/timelines';
 import { refreshNotifications } from '../../actions/notifications';
+import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
-import Status from '../../features/status';
-import GettingStarted from '../../features/getting_started';
-import PublicTimeline from '../../features/public_timeline';
-import CommunityTimeline from '../../features/community_timeline';
-import AccountTimeline from '../../features/account_timeline';
-import AccountGallery from '../../features/account_gallery';
-import HomeTimeline from '../../features/home_timeline';
-import Compose from '../../features/compose';
-import Followers from '../../features/followers';
-import Following from '../../features/following';
-import Reblogs from '../../features/reblogs';
-import Favourites from '../../features/favourites';
-import HashtagTimeline from '../../features/hashtag_timeline';
-import Notifications from '../../features/notifications';
-import FollowRequests from '../../features/follow_requests';
-import GenericNotFound from '../../features/generic_not_found';
-import FavouritedStatuses from '../../features/favourited_statuses';
-import Blocks from '../../features/blocks';
-import Mutes from '../../features/mutes';
-
-// Small wrapper to pass multiColumn to the route components
-const WrappedSwitch = ({ multiColumn, children }) => (
-  <Switch>
-    {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
-  </Switch>
-);
-
-WrappedSwitch.propTypes = {
-  multiColumn: PropTypes.bool,
-  children: PropTypes.node,
-};
-
-// Small Wraper to extract the params from the route and pass
-// them to the rendered component, together with the content to
-// be rendered inside (the children)
-class WrappedRoute extends React.Component {
-
-  static propTypes = {
-    component: PropTypes.func.isRequired,
-    content: PropTypes.node,
-    multiColumn: PropTypes.bool,
-  }
-
-  renderComponent = ({ match: { params } }) => {
-    const { component: Component, content, multiColumn } = this.props;
-
-    return <Component params={params} multiColumn={multiColumn}>{content}</Component>;
-  }
-
-  render () {
-    const { component: Component, content, ...rest } = this.props;
-
-    return <Route {...rest} render={this.renderComponent} />;
-  }
-
-}
+import {
+  Compose,
+  Status,
+  GettingStarted,
+  PublicTimeline,
+  CommunityTimeline,
+  AccountTimeline,
+  AccountGallery,
+  HomeTimeline,
+  Followers,
+  Following,
+  Reblogs,
+  Favourites,
+  HashtagTimeline,
+  Notifications,
+  FollowRequests,
+  GenericNotFound,
+  FavouritedStatuses,
+  Blocks,
+  Mutes,
+} from './util/async-components';
+
+// Dummy import, to make sure that <Status /> ends up in the application bundle.
+// Without this it ends up in ~8 very commonly used bundles.
+import '../../../glitch/components/status';
 
 const mapStateToProps = state => ({
+  systemFontUi: state.getIn(['meta', 'system_font_ui']),
   layout: state.getIn(['local_settings', 'layout']),
   isWide: state.getIn(['local_settings', 'stretch']),
 });
@@ -85,6 +55,7 @@ export default class UI extends React.PureComponent {
     children: PropTypes.node,
     layout: PropTypes.string,
     isWide: PropTypes.bool,
+    systemFontUi: PropTypes.bool,
   };
 
   state = {
@@ -194,8 +165,13 @@ export default class UI extends React.PureComponent {
       }
     };
 
+    const className = classNames('ui', columnsClass(layout), {
+      'wide': isWide,
+      'system-font': this.props.systemFontUi,
+    });
+
     return (
-      <div className={'ui ' + columnsClass(layout) + (isWide ? ' wide' : '')} ref={this.setRef}>
+      <div className={className} ref={this.setRef}>
         <TabsBar />
         <ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
           <WrappedSwitch>
diff --git a/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js
new file mode 100644
index 000000000..c266cd7dc
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js
@@ -0,0 +1,21 @@
+
+// Get the bounding client rect from an IntersectionObserver entry.
+// This is to work around a bug in Chrome: https://crbug.com/737228
+
+let hasBoundingRectBug;
+
+function getRectFromEntry(entry) {
+  if (typeof hasBoundingRectBug !== 'boolean') {
+    const boundingRect = entry.target.getBoundingClientRect();
+    const observerRect = entry.boundingClientRect;
+    hasBoundingRectBug = boundingRect.height !== observerRect.height ||
+      boundingRect.top !== observerRect.top ||
+      boundingRect.width !== observerRect.width ||
+      boundingRect.bottom !== observerRect.bottom ||
+      boundingRect.left !== observerRect.left ||
+      boundingRect.right !== observerRect.right;
+  }
+  return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect;
+}
+
+export default getRectFromEntry;
diff --git a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
index 0e959f9ae..2b24c6583 100644
--- a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
+++ b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
@@ -37,9 +37,18 @@ class IntersectionObserverWrapper {
     }
   }
 
+  unobserve (id, node) {
+    if (this.observer) {
+      delete this.callbacks[id];
+      this.observer.unobserve(node);
+    }
+  }
+
   disconnect () {
     if (this.observer) {
+      this.callbacks = {};
       this.observer.disconnect();
+      this.observer = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
new file mode 100644
index 000000000..ede578e56
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Switch from 'react-router-dom/Switch';
+import Route from 'react-router-dom/Route';
+
+import ColumnLoading from '../components/column_loading';
+import BundleColumnError from '../components/bundle_column_error';
+import BundleContainer from '../containers/bundle_container';
+
+// Small wrapper to pass multiColumn to the route components
+export const WrappedSwitch = ({ multiColumn, children }) => (
+  <Switch>
+    {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
+  </Switch>
+);
+
+WrappedSwitch.propTypes = {
+  multiColumn: PropTypes.bool,
+  children: PropTypes.node,
+};
+
+// Small Wraper to extract the params from the route and pass
+// them to the rendered component, together with the content to
+// be rendered inside (the children)
+export class WrappedRoute extends React.Component {
+
+  static propTypes = {
+    component: PropTypes.func.isRequired,
+    content: PropTypes.node,
+    multiColumn: PropTypes.bool,
+  }
+
+  renderComponent = ({ match }) => {
+    const { component, content, multiColumn } = this.props;
+
+    return (
+      <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
+        {Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>}
+      </BundleContainer>
+    );
+  }
+
+  renderLoading = () => {
+    return <ColumnLoading />;
+  }
+
+  renderError = (props) => {
+    return <BundleColumnError {...props} />;
+  }
+
+  render () {
+    const { component: Component, content, ...rest } = this.props;
+
+    return <Route {...rest} render={this.renderComponent} />;
+  }
+
+}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index c13bc73d3..6992e7e0f 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -18,6 +18,12 @@
   "account.unfollow": "إلغاء المتابعة",
   "account.unmute": "إلغاء الكتم عن @{name}",
   "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "الحسابات المحجوبة",
   "column.community": "الخيط العام المحلي",
   "column.favourites": "المفضلة",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
   "privacy.unlisted.short": "غير مدرج",
   "reply_indicator.cancel": "إلغاء",
-  "report.heading": "تقرير جديد",
   "report.placeholder": "تعليقات إضافية",
   "report.submit": "إرسال",
   "report.target": "إبلاغ",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 3b6f228c6..7a56e1446 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Не следвай",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Отказ",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 8e8c95d56..b2673915a 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Treure silenci de @{name}",
   "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Usuaris bloquejats",
   "column.community": "Línia de temps local",
   "column.favourites": "Favorits",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "No publicar en línies de temps públiques",
   "privacy.unlisted.short": "No llistat",
   "reply_indicator.cancel": "Cancel·lar",
-  "report.heading": "Nou informe",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Enviar",
   "report.target": "Informes",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 55499c0a3..4b62403c3 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blockierte Benutzer",
   "column.community": "Lokale Zeitleiste",
   "column.favourites": "Favoriten",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
   "privacy.unlisted.short": "Nicht gelistet",
   "reply_indicator.cancel": "Abbrechen",
-  "report.heading": "Neue Meldung",
   "report.placeholder": "Zusätzliche Kommentare",
   "report.submit": "Absenden",
   "report.target": "Melden",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index a5ff686a0..36d82ec1a 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -644,6 +644,14 @@
         "id": "getting_started.heading"
       },
       {
+        "defaultMessage": "Home",
+        "id": "tabs_bar.home"
+      },
+      {
+        "defaultMessage": "Notifications",
+        "id": "tabs_bar.notifications"
+      },
+      {
         "defaultMessage": "Federated timeline",
         "id": "navigation_bar.public_timeline"
       },
@@ -959,27 +967,6 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "New report",
-        "id": "report.heading"
-      },
-      {
-        "defaultMessage": "Additional comments",
-        "id": "report.placeholder"
-      },
-      {
-        "defaultMessage": "Submit",
-        "id": "report.submit"
-      },
-      {
-        "defaultMessage": "Reporting",
-        "id": "report.target"
-      }
-    ],
-    "path": "app/javascript/mastodon/features/report/index.json"
-  },
-  {
-    "descriptors": [
-      {
         "defaultMessage": "Delete",
         "id": "status.delete"
       },
@@ -1039,6 +1026,40 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Network error",
+        "id": "bundle_column_error.title"
+      },
+      {
+        "defaultMessage": "Something went wrong while loading this component.",
+        "id": "bundle_column_error.body"
+      },
+      {
+        "defaultMessage": "Try again",
+        "id": "bundle_column_error.retry"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/bundle_column_error.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Something went wrong while loading this component.",
+        "id": "bundle_modal_error.message"
+      },
+      {
+        "defaultMessage": "Try again",
+        "id": "bundle_modal_error.retry"
+      },
+      {
+        "defaultMessage": "Close",
+        "id": "bundle_modal_error.close"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/bundle_modal_error.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Cancel",
         "id": "confirmation_modal.cancel"
       }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index b286ed2d3..cf29e38da 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -141,7 +147,6 @@
   "privacy.unlisted.long": "Do not post to public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Cancel",
-  "report.heading": "Report {target}",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting {target}",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 330fe831d..2648a6840 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Malsekvi",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Loka tempolinio",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Rezigni",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 6469aa6f2..c42930380 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Usuarios bloqueados",
   "column.community": "Historia local",
   "column.favourites": "Favoritos",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "No mostrar en la historia federada",
   "privacy.unlisted.short": "Sin federar",
   "reply_indicator.cancel": "Cancelar",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 3835caab1..c9f1888b5 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -18,6 +18,12 @@
   "account.unfollow": "پایان پیگیری",
   "account.unmute": "باصدا کردن @{name}",
   "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "کاربران مسدودشده",
   "column.community": "نوشته‌های محلی",
   "column.favourites": "پسندیده‌ها",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "عمومی، ولی فهرست نکن",
   "privacy.unlisted.short": "فهرست‌نشده",
   "reply_indicator.cancel": "لغو",
-  "report.heading": "گزارش تازه",
   "report.placeholder": "توضیح اضافه",
   "report.submit": "بفرست",
   "report.target": "گزارش‌دادن",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index dae911799..b836d2f5d 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Lopeta seuraaminen",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Paikallinen aikajana",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Peruuta",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 1a69235c8..eaa01638c 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -7,7 +7,7 @@
   "account.followers": "Abonné⋅e⋅s",
   "account.follows": "Abonnements",
   "account.follows_you": "Vous suit",
-  "account.media": "Media",
+  "account.media": "Média",
   "account.mention": "Mentionner",
   "account.mute": "Masquer",
   "account.posts": "Statuts",
@@ -18,6 +18,12 @@
   "account.unfollow": "Ne plus suivre",
   "account.unmute": "Ne plus masquer",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Comptes bloqués",
   "column.community": "Fil public local",
   "column.favourites": "Favoris",
@@ -31,10 +37,10 @@
   "column_header.unpin": "Retirer",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Paramètres",
-  "compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
+  "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
-  "compose_form.placeholder": "Qu’avez-vous en tête ?",
-  "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
+  "compose_form.placeholder": "Qu’avez-vous en tête ?",
+  "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
   "compose_form.publish": "Pouet ",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marquer le média comme délicat",
@@ -42,13 +48,13 @@
   "compose_form.spoiler_placeholder": "Avertissement",
   "confirmation_modal.cancel": "Annuler",
   "confirmations.block.confirm": "Bloquer",
-  "confirmations.block.message": "Confirmez vous le blocage de {name} ?",
+  "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.message": "Confirmez vous la suppression de ce pouet ?",
   "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 silenciations ciblés sont suffisants et préférables.",
+  "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 silenciations ciblés sont suffisants et préférables.",
   "confirmations.mute.confirm": "Silencer",
-  "confirmations.mute.message": "Confirmez vous la silenciation {name} ?",
+  "confirmations.mute.message": "Confirmez vous la silenciation {name} ?",
   "emoji_button.activity": "Activités",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
@@ -59,20 +65,20 @@
   "emoji_button.search": "Recherche…",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Lieux et voyages",
-  "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
+  "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
   "empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag",
-  "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateurs⋅trices.",
+  "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.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
-  "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs⋅trices d’autres instances pour remplir le fil public.",
+  "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": "Autoriser",
   "follow_request.reject": "Rejeter",
   "getting_started.appsshort": "Applications",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Pour commencer",
   "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
-  "getting_started.userguide": "Guide d'utilisation",
+  "getting_started.userguide": "Guide d’utilisation",
   "home.column_settings.advanced": "Avancé",
   "home.column_settings.basic": "Basique",
   "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
@@ -93,37 +99,37 @@
   "navigation_bar.mutes": "Comptes silencés",
   "navigation_bar.preferences": "Préférences",
   "navigation_bar.public_timeline": "Fil public global",
-  "notification.favourite": "{name} a ajouté à ses favoris :",
+  "notification.favourite": "{name} a ajouté à ses favoris :",
   "notification.follow": "{name} vous suit.",
-  "notification.mention": "{name} vous a mentionné⋅e :",
-  "notification.reblog": "{name} a partagé votre statut :",
+  "notification.mention": "{name} vous a mentionné⋅e :",
+  "notification.reblog": "{name} a partagé votre statut :",
   "notifications.clear": "Nettoyer",
-  "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
+  "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.column_settings.alert": "Notifications locales",
-  "notifications.column_settings.favourite": "Favoris :",
-  "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
-  "notifications.column_settings.mention": "Mentions :",
-  "notifications.column_settings.reblog": "Partages :",
+  "notifications.column_settings.favourite": "Favoris :",
+  "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
+  "notifications.column_settings.mention": "Mentions :",
+  "notifications.column_settings.reblog": "Partages :",
   "notifications.column_settings.show": "Afficher dans la colonne",
   "notifications.column_settings.sound": "Émettre un son",
   "onboarding.done": "Effectué",
   "onboarding.next": "Suivant",
-  "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.",
-  "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez",
+  "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateur⋅ice⋅s de {domain}.",
+  "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s que vous suivez",
   "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous",
   "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
-  "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅trice complet est {handle}",
-  "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
+  "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}",
+  "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
   "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}",
   "onboarding.page_six.almost_done": "Nous y sommes presque…",
   "onboarding.page_six.appetoot": "Bon Appetoot!",
   "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!",
   "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
   "onboarding.page_six.guidelines": "règles de la communauté",
-  "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
+  "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
   "onboarding.page_six.various_app": "applications mobiles",
   "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.",
-  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅trice complet.",
+  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅ice complet.",
   "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
   "onboarding.skip": "Passer",
   "privacy.change": "Ajuster la confidentialité du message",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ne pas afficher dans les fils publics",
   "privacy.unlisted.short": "Non-listé",
   "reply_indicator.cancel": "Annuler",
-  "report.heading": "Nouveau signalement",
   "report.placeholder": "Commentaires additionnels",
   "report.submit": "Envoyer",
   "report.target": "Signalement",
@@ -151,7 +156,7 @@
   "status.mute_conversation": "Masquer la conversation",
   "status.open": "Déplier ce statut",
   "status.reblog": "Partager",
-  "status.reblogged_by": "{name} a partagé :",
+  "status.reblogged_by": "{name} a partagé :",
   "status.reply": "Répondre",
   "status.replyAll": "Répondre au fil",
   "status.report": "Signaler @{name}",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index db3d00394..98c7ea021 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -18,6 +18,12 @@
   "account.unfollow": "הפסקת מעקב",
   "account.unmute": "הפסקת השתקת @{name}",
   "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "חסימות",
   "column.community": "ציר זמן מקומי",
   "column.favourites": "חיבובים",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים",
   "privacy.unlisted.short": "לא לפיד הכללי",
   "reply_indicator.cancel": "ביטול",
-  "report.heading": "דווח חדש",
   "report.placeholder": "הערות נוספות",
   "report.submit": "שליחה",
   "report.target": "דיווח",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index f85eb8a3f..fdf5c11c0 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Prestani slijediti",
   "account.unmute": "Poništi utišavanje @{name}",
   "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blokirani korisnici",
   "column.community": "Lokalni timeline",
   "column.favourites": "Favoriti",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Otkaži",
-  "report.heading": "Nova prijava",
   "report.placeholder": "Dodatni komentari",
   "report.submit": "Pošalji",
   "report.target": "Prijavljivanje",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 350410c4b..baf762c8d 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Követés abbahagyása",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Mégsem",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 6e9bc5ba9..6f6d688e9 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Berhenti mengikuti",
   "account.unmute": "Berhenti membisukan @{name}",
   "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Pengguna diblokir",
   "column.community": "Linimasa Lokal",
   "column.favourites": "Favorit",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik",
   "privacy.unlisted.short": "Tak Terdaftar",
   "reply_indicator.cancel": "Batal",
-  "report.heading": "Laporan baru",
   "report.placeholder": "Komentar tambahan",
   "report.submit": "Kirim",
   "report.target": "Melaporkan",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 005dd4f56..25e0adc8a 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Ne plus sequar",
   "account.unmute": "Ne plus celar @{name}",
   "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blokusita uzeri",
   "column.community": "Lokala tempolineo",
   "column.favourites": "Favorati",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ne montrar en publika tempolinei",
   "privacy.unlisted.short": "Ne enlistigota",
   "reply_indicator.cancel": "Nihiligar",
-  "report.heading": "Nova denunco",
   "report.placeholder": "Plusa komenti",
   "report.submit": "Sendar",
   "report.target": "Denuncante",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 4a5b218e8..4881b0f08 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Non seguire",
   "account.unmute": "Non silenziare @{name}",
   "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Utenti bloccati",
   "column.community": "Timeline locale",
   "column.favourites": "Apprezzati",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Non mostrare sulla timeline pubblica",
   "privacy.unlisted.short": "Non elencato",
   "reply_indicator.cancel": "Annulla",
-  "report.heading": "Nuova segnalazione",
   "report.placeholder": "Commenti aggiuntivi",
   "report.submit": "Invia",
   "report.target": "Invio la segnalazione",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index cb8074b5d..f62072852 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -18,6 +18,12 @@
   "account.unfollow": "フォロー解除",
   "account.unmute": "ミュート解除",
   "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
+  "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
+  "bundle_column_error.retry": "再試行",
+  "bundle_column_error.title": "ネットワークエラー",
+  "bundle_modal_error.close": "閉じる",
+  "bundle_modal_error.message": "コンポーネントの読み込み中に問題が発生しました。",
+  "bundle_modal_error.retry": "再試行",
   "column.blocks": "ブロックしたユーザー",
   "column.community": "ローカルタイムライン",
   "column.favourites": "お気に入り",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "公開TLで表示しない",
   "privacy.unlisted.short": "未収載",
   "reply_indicator.cancel": "キャンセル",
-  "report.heading": "新規通報",
   "report.placeholder": "コメント",
   "report.submit": "通報する",
   "report.target": "問題のユーザー",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
new file mode 100644
index 000000000..5e1aaac85
--- /dev/null
+++ b/app/javascript/mastodon/locales/ko.json
@@ -0,0 +1,181 @@
+{
+  "account.block": "차단",
+  "account.block_domain": "{domain} 전체를 숨김",
+  "account.disclaimer": "이 사용자는 다른 인스턴스에 소속되어 있으므로, 수치가 정확하지 않을 수도 있습니다.",
+  "account.edit_profile": "프로필 편집",
+  "account.follow": "팔로우",
+  "account.followers": "팔로워",
+  "account.follows": "팔로우",
+  "account.follows_you": "날 팔로우합니다",
+  "account.media": "미디어",
+  "account.mention": "답장",
+  "account.mute": "뮤트",
+  "account.posts": "포스트",
+  "account.report": "신고",
+  "account.requested": "승인 대기 중",
+  "account.unblock": "차단 해제",
+  "account.unblock_domain": "{domain} 숨김 해제",
+  "account.unfollow": "팔로우 해제",
+  "account.unmute": "뮤트 해제",
+  "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
+  "column.blocks": "차단 중인 사용자",
+  "column.community": "로컬 타임라인",
+  "column.favourites": "즐겨찾기",
+  "column.follow_requests": "팔로우 요청",
+  "column.home": "홈",
+  "column.mutes": "뮤트 중인 사용자",
+  "column.notifications": "알림",
+  "column.public": "연합 타임라인",
+  "column_back_button.label": "돌아가기",
+  "column_header.pin": "고정하기",
+  "column_header.unpin": "고정 해제",
+  "column_subheading.navigation": "내비게이션",
+  "column_subheading.settings": "설정",
+  "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
+  "compose_form.lock_disclaimer.lock": "비공개",
+  "compose_form.placeholder": "지금 무엇을 하고 있나요?",
+  "compose_form.privacy_disclaimer": "이 계정의 비공개 포스트는 멘션된 사용자가 소속된 {domains}으로 전송됩니다. {domainsCount, plural, one {이 서버를} other {이 서버들을}} 신뢰할 수 있습니까? 포스팅의 프라이버시 보호는 Mastodon 서버에서만 유효합니다. {domains}가 Mastodon 인스턴스가 아닐 경우, 이 투고가 사적인 것으로 취급되지 않은 채 부스트 되거나 원하지 않는 사용자에게 보여질 가능성이 있습니다.",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
+  "compose_form.spoiler": "텍스트 숨기기",
+  "compose_form.spoiler_placeholder": "경고",
+  "confirmation_modal.cancel": "취소",
+  "confirmations.block.confirm": "차단",
+  "confirmations.block.message": "정말로 {name}를 차단하시겠습니까?",
+  "confirmations.delete.confirm": "삭제",
+  "confirmations.delete.message": "정말로 삭제하시겠습니까?",
+  "confirmations.domain_block.confirm": "도메인 전체를 숨김",
+  "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
+  "confirmations.mute.confirm": "뮤트",
+  "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
+  "emoji_button.activity": "활동",
+  "emoji_button.flags": "국기",
+  "emoji_button.food": "음식",
+  "emoji_button.label": "emoji를 추가",
+  "emoji_button.nature": "자연",
+  "emoji_button.objects": "물건",
+  "emoji_button.people": "사람들",
+  "emoji_button.search": "검색...",
+  "emoji_button.symbols": "기호",
+  "emoji_button.travel": "여행과 장소",
+  "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
+  "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
+  "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
+  "empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.",
+  "empty_column.home.public_timeline": "연합 타임라인",
+  "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
+  "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
+  "follow_request.authorize": "허가",
+  "follow_request.reject": "거부",
+  "getting_started.appsshort": "어플리케이션",
+  "getting_started.faq": "자주 있는 질문",
+  "getting_started.heading": "시작",
+  "getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.",
+  "getting_started.userguide": "사용자 가이드",
+  "home.column_settings.advanced": "고급 사용자용",
+  "home.column_settings.basic": "기본 설정",
+  "home.column_settings.filter_regex": "정규 표현식으로 필터링",
+  "home.column_settings.show_reblogs": "부스트 표시",
+  "home.column_settings.show_replies": "답글 표시",
+  "home.settings": "컬럼 설정",
+  "lightbox.close": "닫기",
+  "loading_indicator.label": "불러오는 중...",
+  "media_gallery.toggle_visible": "표시 전환",
+  "missing_indicator.label": "찾을 수 없습니다",
+  "navigation_bar.blocks": "차단한 사용자",
+  "navigation_bar.community_timeline": "로컬 타임라인",
+  "navigation_bar.edit_profile": "프로필 편집",
+  "navigation_bar.favourites": "즐겨찾기",
+  "navigation_bar.follow_requests": "팔로우 요청",
+  "navigation_bar.info": "이 인스턴스에 대해서",
+  "navigation_bar.logout": "로그아웃",
+  "navigation_bar.mutes": "뮤트 중인 사용자",
+  "navigation_bar.preferences": "사용자 설정",
+  "navigation_bar.public_timeline": "연합 타임라인",
+  "notification.favourite": "{name}님이 즐겨찾기 했습니다",
+  "notification.follow": "{name}님이 나를 팔로우 했습니다",
+  "notification.mention": "{name}님이 답글을 보냈습니다",
+  "notification.reblog": "{name}님이 부스트 했습니다",
+  "notifications.clear": "알림 지우기",
+  "notifications.clear_confirmation": "정말로 알림을 삭제하시겠습니까?",
+  "notifications.column_settings.alert": "데스크탑 알림",
+  "notifications.column_settings.favourite": "즐겨찾기",
+  "notifications.column_settings.follow": "새 팔로워",
+  "notifications.column_settings.mention": "답글",
+  "notifications.column_settings.reblog": "부스트",
+  "notifications.column_settings.show": "컬럼에 표시",
+  "notifications.column_settings.sound": "효과음 재생",
+  "onboarding.done": "완료",
+  "onboarding.next": "다음",
+  "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.",
+  "onboarding.page_four.home": "홈 타임라인에서는 내가 팔로우 중인 사람들의 포스트를 표시합니다.",
+  "onboarding.page_four.notifications": "알림에서는 다른 사람들과의 연결을 표시합니다.",
+  "onboarding.page_one.federation": "Mastodon은 누구나 참가할 수 있는 SNS입니다.",
+  "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. 당신의 유저 이름은 {handle} 입니다.",
+  "onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
+  "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
+  "onboarding.page_six.almost_done": "이상입니다.",
+  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
+  "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
+  "onboarding.page_six.guidelines": "커뮤니티 가이드라인",
+  "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.",
+  "onboarding.page_six.various_app": "다양한 모바일 어플리케이션",
+  "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.",
+  "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.",
+  "onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.",
+  "onboarding.skip": "건너뛰기",
+  "privacy.change": "포스트의 프라이버시 설정을 변경",
+  "privacy.direct.long": "멘션한 사용자에게만 공개",
+  "privacy.direct.short": "다이렉트",
+  "privacy.private.long": "팔로워에게만 공개",
+  "privacy.private.short": "비공개",
+  "privacy.public.long": "공개 타임라인에 표시",
+  "privacy.public.short": "공개",
+  "privacy.unlisted.long": "공개 타임라인에 표시하지 않음",
+  "privacy.unlisted.short": "Unlisted",
+  "reply_indicator.cancel": "취소",
+  "report.placeholder": "코멘트",
+  "report.submit": "신고하기",
+  "report.target": "문제가 된 사용자",
+  "search.placeholder": "검색",
+  "search_results.total": "{count, number}건의 결과",
+  "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
+  "status.delete": "삭제",
+  "status.favourite": "즐겨찾기",
+  "status.load_more": "더 보기",
+  "status.media_hidden": "미디어 숨겨짐",
+  "status.mention": "답장",
+  "status.mute_conversation": "이 대화를 뮤트",
+  "status.open": "상세 정보 표시",
+  "status.reblog": "부스트",
+  "status.reblogged_by": "{name}님이 부스트 했습니다",
+  "status.reply": "답장",
+  "status.replyAll": "전원에게 답장",
+  "status.report": "신고",
+  "status.sensitive_toggle": "클릭해서 표시하기",
+  "status.sensitive_warning": "민감한 미디어",
+  "status.show_less": "숨기기",
+  "status.show_more": "더 보기",
+  "status.unmute_conversation": "이 대화의 뮤트 해제하기",
+  "tabs_bar.compose": "포스트",
+  "tabs_bar.federated_timeline": "연합",
+  "tabs_bar.home": "홈",
+  "tabs_bar.local_timeline": "로컬",
+  "tabs_bar.notifications": "알림",
+  "upload_area.title": "드래그 & 드롭으로 업로드",
+  "upload_button.label": "미디어 추가",
+  "upload_form.undo": "재시도",
+  "upload_progress.label": "업로드 중...",
+  "video_player.expand": "동영상 자세히 보기",
+  "video_player.toggle_sound": "소리 토글하기",
+  "video_player.toggle_visible": "표시 전환",
+  "video_player.video_error": "동영상 재생에 실패했습니다"
+}
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 38ca6518a..479d157f3 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -10,7 +10,7 @@
   "account.media": "Media",
   "account.mention": "Vermeld @{name}",
   "account.mute": "Negeer @{name}",
-  "account.posts": "Berichten",
+  "account.posts": "Toots",
   "account.report": "Rapporteer @{name}",
   "account.requested": "Wacht op goedkeuring",
   "account.unblock": "Deblokkeer @{name}",
@@ -18,11 +18,17 @@
   "account.unfollow": "Ontvolgen",
   "account.unmute": "@{name} niet meer negeren",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Geblokkeerde gebruikers",
   "column.community": "Lokale tijdlijn",
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
-  "column.home": "Jouw tijdlijn",
+  "column.home": "Start",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.public": "Globale tijdlijn",
@@ -62,7 +68,7 @@
   "empty_column.community": "De lokale tijdlijn is leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
   "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.inactivity": "Jouw tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
+  "empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
   "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
   "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
@@ -109,7 +115,7 @@
   "onboarding.done": "Klaar",
   "onboarding.next": "Volgende",
   "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.",
-  "onboarding.page_four.home": "Jouw tijdlijn laat toots zien van mensen die jij volgt.",
+  "onboarding.page_four.home": "Deze tijdlijn laat toots zien van mensen die jij volgt.",
   "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.",
   "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.",
   "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen",
   "privacy.unlisted.short": "Minder openbaar",
   "reply_indicator.cancel": "Annuleren",
-  "report.heading": "Rapporteren",
   "report.placeholder": "Extra opmerkingen",
   "report.submit": "Verzenden",
   "report.target": "Rapporteren van",
@@ -162,7 +167,7 @@
   "status.unmute_conversation": "Conversatie niet meer negeren",
   "tabs_bar.compose": "Schrijven",
   "tabs_bar.federated_timeline": "Globaal",
-  "tabs_bar.home": "Jouw tijdlijn",
+  "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
   "upload_area.title": "Hierin slepen om te uploaden",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index a3c956279..4bbf14938 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Avfølg",
   "account.unmute": "Avdemp @{name}",
   "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blokkerte brukere",
   "column.community": "Lokal tidslinje",
   "column.favourites": "Likt",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
   "privacy.unlisted.short": "Uoppført",
   "reply_indicator.cancel": "Avbryt",
-  "report.heading": "Ny rapport",
   "report.placeholder": "Tilleggskommentarer",
   "report.submit": "Send inn",
   "report.target": "Rapporterer",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index a2a82ae9f..2c119ef41 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Quitar de sègre",
   "account.unmute": "Quitar de rescondre @{name}",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Personas blocadas",
   "column.community": "Flux d’actualitat public local",
   "column.favourites": "Favorits",
@@ -34,7 +40,7 @@
   "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
   "compose_form.lock_disclaimer.lock": "clavat",
   "compose_form.placeholder": "A de qué pensatz ?",
-  "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
+  "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
   "compose_form.publish": "Tut",
   "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive": "Marcar lo mèdia coma sensible",
@@ -51,7 +57,7 @@
   "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
   "emoji_button.activity": "Activitat",
   "emoji_button.flags": "Drapèus",
-  "emoji_button.food": "Manjar e beure",
+  "emoji_button.food": "Beure e manjar",
   "emoji_button.label": "Inserir un emoji",
   "emoji_button.nature": "Natura",
   "emoji_button.objects": "Objèctes",
@@ -136,10 +142,9 @@
   "privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
   "privacy.unlisted.short": "Pas-listat",
   "reply_indicator.cancel": "Anullar",
-  "report.heading": "Nòu senhalament",
   "report.placeholder": "Comentaris addicionals",
-  "report.submit": "Mandat",
-  "report.target": "Senhalament",
+  "report.submit": "Mandar",
+  "report.target": "Senhalar {target}",
   "search.placeholder": "Recercar",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index bf425501f..ac63ec40f 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -6,7 +6,7 @@
   "account.follow": "Obserwuj",
   "account.followers": "Obserwujący",
   "account.follows": "Obserwacje",
-  "account.follows_you": "Obserwują cię",
+  "account.follows_you": "Obserwuje cię",
   "account.media": "Media",
   "account.mention": "Wspomnij o @{name}",
   "account.mute": "Wycisz @{name}",
@@ -18,6 +18,12 @@
   "account.unfollow": "Przestań obserwować",
   "account.unmute": "Cofnij wyciszenie @{name}",
   "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
+  "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
+  "bundle_column_error.retry": "Spróbuj ponownie",
+  "bundle_column_error.title": "Błąd sieci",
+  "bundle_modal_error.close": "Zamknij",
+  "bundle_modal_error.message": "Coś poszło nie tak podczas ładowania tego składnika.",
+  "bundle_modal_error.retry": "Spróbuj ponownie",
   "column.blocks": "Zablokowani użytkownicy",
   "column.community": "Lokalna oś czasu",
   "column.favourites": "Ulubione",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
   "privacy.unlisted.short": "Niewidoczne",
   "reply_indicator.cancel": "Anuluj",
-  "report.heading": "Zgłoś {target}",
   "report.placeholder": "Dodatkowe komentarze",
   "report.submit": "Wyślij",
   "report.target": "Zgłaszanie {target}",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 5e5834a0e..b199a39ce 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Utilizadores Bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
@@ -72,7 +78,6 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Primeiros passos",
   "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "User Guide",
   "home.column_settings.advanced": "Avançado",
   "home.column_settings.basic": "Básico",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "Partilhas:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
-  "notifications.settings": "Parâmetros da listagem de Notificações",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "Não publicar nos feeds públicos",
   "privacy.unlisted.short": "Não listar",
   "reply_indicator.cancel": "Cancelar",
-  "report.heading": "Nova denúncia",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 4ebfe0c60..b199a39ce 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Utilizadores Bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Não publicar nos feeds públicos",
   "privacy.unlisted.short": "Não listar",
   "reply_indicator.cancel": "Cancelar",
-  "report.heading": "Nova denúncia",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index f561f0151..f9f48a48d 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Отписаться",
   "account.unmute": "Снять глушение",
   "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Список блокировки",
   "column.community": "Локальная лента",
   "column.favourites": "Понравившееся",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Не показывать в лентах",
   "privacy.unlisted.short": "Скрытый",
   "reply_indicator.cancel": "Отмена",
-  "report.heading": "Новая жалоба",
   "report.placeholder": "Комментарий",
   "report.submit": "Отправить",
   "report.target": "Жалуемся на",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 608d911e9..8a39beacb 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not post to public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Cancel",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 7512971f5..203e4a09e 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Takipten vazgeç",
   "account.unmute": "Sesi aç @{name}",
   "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Engellenen kullanıcılar",
   "column.community": "Yerel zaman tüneli",
   "column.favourites": "Favoriler",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme",
   "privacy.unlisted.short": "Listelenmemiş",
   "reply_indicator.cancel": "İptal",
-  "report.heading": "Yeni rapor",
   "report.placeholder": "Ek yorumlar",
   "report.submit": "Gönder",
   "report.target": "Raporlama",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index a117c854b..c0f4a8dbb 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Відписатися",
   "account.unmute": "Зняти глушення",
   "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "Заблоковані користувачі",
   "column.community": "Локальна стрічка",
   "column.favourites": "Вподобане",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Не показувати у публічних стрічках",
   "privacy.unlisted.short": "Прихований",
   "reply_indicator.cancel": "Відмінити",
-  "report.heading": "Нова скарга",
   "report.placeholder": "Додаткові коментарі",
   "report.submit": "Відправити",
   "report.target": "Скаржимося на",
diff --git a/app/javascript/mastodon/locales/whitelist_ko.json b/app/javascript/mastodon/locales/whitelist_ko.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_ko.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_zh-HK.json b/app/javascript/mastodon/locales/whitelist_zh-HK.json
index d5fddc3bc..0d4f101c7 100644
--- a/app/javascript/mastodon/locales/whitelist_zh-HK.json
+++ b/app/javascript/mastodon/locales/whitelist_zh-HK.json
@@ -1,3 +1,2 @@
 [
-  "getting_started.support"
 ]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 68648f2dd..998e1c8da 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -18,6 +18,12 @@
   "account.unfollow": "取消关注",
   "account.unmute": "取消 @{name} 的静音",
   "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "屏蔽用户",
   "column.community": "本站时间轴",
   "column.favourites": "赞过的嘟文",
@@ -72,7 +78,6 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "开始使用",
   "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "User Guide",
   "home.column_settings.advanced": "高端",
   "home.column_settings.basic": "基本",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "你的嘟文被转嘟:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.settings": "字段设置",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "公开,但不在公共时间轴显示",
   "privacy.unlisted.short": "公开",
   "reply_indicator.cancel": "取消",
-  "report.heading": "举报",
   "report.placeholder": "额外消息",
   "report.submit": "提交",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index a9844a625..1079d5429 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -18,6 +18,12 @@
   "account.unfollow": "取消關注",
   "account.unmute": "取消 @{name} 的靜音",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "封鎖用戶",
   "column.community": "本站時間軸",
   "column.favourites": "喜歡的文章",
@@ -72,7 +78,6 @@
   "getting_started.faq": "常見問題",
   "getting_started.heading": "開始使用",
   "getting_started.open_source_notice": "Mastodon(萬象)是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "使用指南",
   "home.column_settings.advanced": "進階",
   "home.column_settings.basic": "基本",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "轉推你的文章:",
   "notifications.column_settings.show": "在通知欄顯示",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.settings": "欄位設定",
   "onboarding.done": "開始使用",
   "onboarding.next": "繼續",
   "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "公開,但不在公共時間軸顯示",
   "privacy.unlisted.short": "公開",
   "reply_indicator.cancel": "取消",
-  "report.heading": "舉報",
   "report.placeholder": "額外訊息",
   "report.submit": "提交",
   "report.target": "舉報",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 5497becf0..6240b8879 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -18,6 +18,12 @@
   "account.unfollow": "取消關注",
   "account.unmute": "不再消音 @{name}",
   "boost_modal.combo": "下次你可以按 {combo} 來跳過",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
   "column.blocks": "封鎖的使用者",
   "column.community": "本地時間軸",
   "column.favourites": "最愛",
@@ -72,7 +78,6 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "馬上開始",
   "getting_started.open_source_notice": "Mastodon 是開源軟體。你可以在 GitHub {github} 上做出貢獻或是回報問題。",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "使用者指南",
   "home.column_settings.advanced": "進階",
   "home.column_settings.basic": "基本",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "轉推:",
   "notifications.column_settings.show": "顯示在欄位中",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.settings": "欄位設定",
   "onboarding.done": "完成",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "不要貼到公開時間軸",
   "privacy.unlisted.short": "不列出來",
   "reply_indicator.cancel": "取消",
-  "report.heading": "新的通報",
   "report.placeholder": "更多訊息",
   "report.submit": "送出",
   "report.target": "通報中",
diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index f14b6a825..90c2c5da2 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -1,9 +1,5 @@
 const perf = require('./performance');
 
-// import default stylesheet with variables
-require('font-awesome/css/font-awesome.css');
-require('mastodon-application-style');
-
 function onDomContentLoaded(callback) {
   if (document.readyState !== 'loading') {
     callback();
@@ -20,6 +16,14 @@ function main() {
 
   require.context('../images/', true);
 
+  if (window.history && history.replaceState) {
+    const { pathname, search, hash } = window.location;
+    const path = pathname + search + hash;
+    if (!(/^\/web[$/]/).test(path)) {
+      history.replaceState(null, document.title, `/web${path}`);
+    }
+  }
+
   onDomContentLoaded(() => {
     const mountNode = document.getElementById('mastodon');
     const props = JSON.parse(mountNode.getAttribute('data-props'));
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 7b7074317..4d7c3adc9 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -44,7 +44,7 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeAccount = (state, account) => {
   account = { ...account };
@@ -53,7 +53,7 @@ const normalizeAccount = (state, account) => {
   delete account.following_count;
   delete account.statuses_count;
 
-  return state.set(account.id, Immutable.fromJS(account));
+  return state.set(account.id, fromJS(account));
 };
 
 const normalizeAccounts = (state, accounts) => {
@@ -82,7 +82,7 @@ const normalizeAccountsFromStatuses = (state, statuses) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function accounts(state = initialState, action) {
   switch(action.type) {
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index eb8a4f83d..4423e1b50 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -46,9 +46,9 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
-const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS({
+const normalizeAccount = (state, account) => state.set(account.id, fromJS({
   followers_count: account.followers_count,
   following_count: account.following_count,
   statuses_count: account.statuses_count,
@@ -80,12 +80,12 @@ const normalizeAccountsFromStatuses = (state, statuses) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function accountsCounters(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
-    return state.merge(action.state.get('accounts').map(item => Immutable.fromJS({
+    return state.merge(action.state.get('accounts').map(item => fromJS({
       followers_count: item.get('followers_count'),
       following_count: item.get('following_count'),
       statuses_count: item.get('statuses_count'),
diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js
index aaea9775f..089d920c3 100644
--- a/app/javascript/mastodon/reducers/alerts.js
+++ b/app/javascript/mastodon/reducers/alerts.js
@@ -3,14 +3,14 @@ import {
   ALERT_DISMISS,
   ALERT_CLEAR,
 } from '../actions/alerts';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.List([]);
+const initialState = ImmutableList([]);
 
 export default function alerts(state = initialState, action) {
   switch(action.type) {
   case ALERT_SHOW:
-    return state.push(Immutable.Map({
+    return state.push(ImmutableMap({
       key: state.size > 0 ? state.last().get('key') + 1 : 0,
       title: action.title,
       message: action.message,
diff --git a/app/javascript/mastodon/reducers/cards.js b/app/javascript/mastodon/reducers/cards.js
index 3c9395011..4d86b0d7e 100644
--- a/app/javascript/mastodon/reducers/cards.js
+++ b/app/javascript/mastodon/reducers/cards.js
@@ -1,13 +1,13 @@
 import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards';
 
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function cards(state = initialState, action) {
   switch(action.type) {
   case STATUS_CARD_FETCH_SUCCESS:
-    return state.set(action.id, Immutable.fromJS(action.card));
+    return state.set(action.id, fromJS(action.card));
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 21d801f2a..6ac7b4b4a 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -25,12 +25,12 @@ import {
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
 import uuid from '../uuid';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   mounted: false,
-  advanced_options: Immutable.Map({
+  advanced_options: ImmutableMap({
     do_not_federate: false,
   }),
   sensitive: false,
@@ -44,20 +44,21 @@ const initialState = Immutable.Map({
   is_submitting: false,
   is_uploading: false,
   progress: 0,
-  media_attachments: Immutable.List(),
+  media_attachments: ImmutableList(),
   suggestion_token: null,
-  suggestions: Immutable.List(),
+  suggestions: ImmutableList(),
   me: null,
-  default_advanced_options: Immutable.Map({
+  default_advanced_options: ImmutableMap({
     do_not_federate: false,
   }),
   default_privacy: 'public',
+  default_sensitive: false,
   resetFileKey: Math.floor((Math.random() * 0x10000)),
   idempotencyKey: null,
 });
 
 function statusToTextMentions(state, status) {
-  let set = Immutable.OrderedSet([]);
+  let set = ImmutableOrderedSet([]);
   let me  = state.get('me');
 
   if (status.getIn(['account', 'id']) !== me) {
@@ -83,6 +84,8 @@ function clearAll(state) {
 };
 
 function appendMedia(state, media) {
+  const prevSize = state.get('media_attachments').size;
+
   return state.withMutations(map => {
     map.update('media_attachments', list => list.push(media));
     map.set('is_uploading', false);
@@ -90,6 +93,10 @@ function appendMedia(state, media) {
     map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`);
     map.set('focusDate', new Date());
     map.set('idempotencyKey', uuid());
+
+    if (prevSize === 0 && state.get('default_sensitive')) {
+      map.set('sensitive', true);
+    }
   });
 };
 
@@ -112,7 +119,7 @@ const insertSuggestion = (state, position, token, completion) => {
   return state.withMutations(map => {
     map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
     map.set('suggestion_token', null);
-    map.update('suggestions', Immutable.List(), list => list.clear());
+    map.update('suggestions', ImmutableList(), list => list.clear());
     map.set('focusDate', new Date());
     map.set('idempotencyKey', uuid());
   });
@@ -180,7 +187,7 @@ export default function compose(state = initialState, action) {
       map.set('in_reply_to', action.status.get('id'));
       map.set('text', statusToTextMentions(state, action.status));
       map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
-      map.set('advanced_options', new Immutable.Map({
+      map.set('advanced_options', new ImmutableMap({
         do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')),
       }));
       map.set('focusDate', new Date());
@@ -216,7 +223,7 @@ export default function compose(state = initialState, action) {
       map.set('is_uploading', true);
     });
   case COMPOSE_UPLOAD_SUCCESS:
-    return appendMedia(state, Immutable.fromJS(action.media));
+    return appendMedia(state, fromJS(action.media));
   case COMPOSE_UPLOAD_FAIL:
     return state.set('is_uploading', false);
   case COMPOSE_UPLOAD_UNDO:
@@ -229,9 +236,9 @@ export default function compose(state = initialState, action) {
       .set('focusDate', new Date())
       .set('idempotencyKey', uuid());
   case COMPOSE_SUGGESTIONS_CLEAR:
-    return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null);
+    return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
   case COMPOSE_SUGGESTIONS_READY:
-    return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
+    return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
   case COMPOSE_SUGGESTION_SELECT:
     return insertSuggestion(state, action.position, action.token, action.completion);
   case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js
index 8a24f5f7a..9bfc09aa7 100644
--- a/app/javascript/mastodon/reducers/contexts.js
+++ b/app/javascript/mastodon/reducers/contexts.js
@@ -1,10 +1,10 @@
 import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
 import { TIMELINE_DELETE } from '../actions/timelines';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
-const initialState = Immutable.Map({
-  ancestors: Immutable.Map(),
-  descendants: Immutable.Map(),
+const initialState = ImmutableMap({
+  ancestors: ImmutableMap(),
+  descendants: ImmutableMap(),
 });
 
 const normalizeContext = (state, id, ancestors, descendants) => {
@@ -18,12 +18,12 @@ const normalizeContext = (state, id, ancestors, descendants) => {
 };
 
 const deleteFromContexts = (state, id) => {
-  state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
-    state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
+  state.getIn(['descendants', id], ImmutableList()).forEach(descendantId => {
+    state = state.updateIn(['ancestors', descendantId], ImmutableList(), list => list.filterNot(itemId => itemId === id));
   });
 
-  state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => {
-    state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
+  state.getIn(['ancestors', id], ImmutableList()).forEach(ancestorId => {
+    state = state.updateIn(['descendants', ancestorId], ImmutableList(), list => list.filterNot(itemId => itemId === id));
   });
 
   state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]);
@@ -34,7 +34,7 @@ const deleteFromContexts = (state, id) => {
 export default function contexts(state = initialState, action) {
   switch(action.type) {
   case CONTEXT_FETCH_SUCCESS:
-    return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
+    return normalizeContext(state, action.id, fromJS(action.ancestors), fromJS(action.descendants));
   case TIMELINE_DELETE:
     return deleteFromContexts(state, action.id);
   default:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index a3f5cb58e..35f30f601 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -1,7 +1,6 @@
 import { combineReducers } from 'redux-immutable';
 import timelines from './timelines';
 import meta from './meta';
-import compose from './compose';
 import alerts from './alerts';
 import { loadingBarReducer } from 'react-redux-loading-bar';
 import modal from './modal';
@@ -9,21 +8,21 @@ import user_lists from './user_lists';
 import accounts from './accounts';
 import accounts_counters from './accounts_counters';
 import statuses from './statuses';
-import media_attachments from './media_attachments';
 import relationships from './relationships';
-import search from './search';
-import notifications from './notifications';
 import settings from './settings';
 import local_settings from '../../glitch/reducers/local_settings';
 import status_lists from './status_lists';
 import cards from './cards';
 import reports from './reports';
 import contexts from './contexts';
+import compose from './compose';
+import search from './search';
+import media_attachments from './media_attachments';
+import notifications from './notifications';
 
-export default combineReducers({
+const reducers = {
   timelines,
   meta,
-  compose,
   alerts,
   loadingBar: loadingBarReducer,
   modal,
@@ -31,14 +30,17 @@ export default combineReducers({
   status_lists,
   accounts,
   accounts_counters,
-  media_attachments,
   statuses,
   relationships,
-  search,
-  notifications,
   settings,
   local_settings,
   cards,
   reports,
   contexts,
-});
+  compose,
+  search,
+  media_attachments,
+  notifications,
+};
+
+export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js
index 85bea4f0b..24119f628 100644
--- a/app/javascript/mastodon/reducers/media_attachments.js
+++ b/app/javascript/mastodon/reducers/media_attachments.js
@@ -1,7 +1,7 @@
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap } from 'immutable';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   accept_content_types: [],
 });
 
diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js
index 1551228ec..119ef9d8f 100644
--- a/app/javascript/mastodon/reducers/meta.js
+++ b/app/javascript/mastodon/reducers/meta.js
@@ -1,7 +1,7 @@
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap } from 'immutable';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   streaming_api_base_url: null,
   access_token: null,
   me: null,
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index 0c1cf5b0f..0063d24e4 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -11,10 +11,10 @@ import {
 } from '../actions/notifications';
 import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 import { TIMELINE_DELETE } from '../actions/timelines';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
-  items: Immutable.List(),
+const initialState = ImmutableMap({
+  items: ImmutableList(),
   next: null,
   top: true,
   unread: 0,
@@ -22,7 +22,7 @@ const initialState = Immutable.Map({
   isLoading: true,
 });
 
-const notificationToMap = notification => Immutable.Map({
+const notificationToMap = notification => ImmutableMap({
   id: notification.id,
   type: notification.type,
   account: notification.account.id,
@@ -46,7 +46,7 @@ const normalizeNotification = (state, notification) => {
 };
 
 const normalizeNotifications = (state, notifications, next) => {
-  let items    = Immutable.List();
+  let items    = ImmutableList();
   const loaded = state.get('loaded');
 
   notifications.forEach((n, i) => {
@@ -64,7 +64,7 @@ const normalizeNotifications = (state, notifications, next) => {
 };
 
 const appendNormalizedNotifications = (state, notifications, next) => {
-  let items = Immutable.List();
+  let items = ImmutableList();
 
   notifications.forEach((n, i) => {
     items = items.set(i, notificationToMap(n));
@@ -110,7 +110,7 @@ export default function notifications(state = initialState, action) {
   case ACCOUNT_BLOCK_SUCCESS:
     return filterNotifications(state, action.relationship);
   case NOTIFICATIONS_CLEAR:
-    return state.set('items', Immutable.List()).set('next', null);
+    return state.set('items', ImmutableList()).set('next', null);
   case TIMELINE_DELETE:
     return deleteByStatus(state, action.id);
   default:
diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js
index b6607860c..c7b04a668 100644
--- a/app/javascript/mastodon/reducers/relationships.js
+++ b/app/javascript/mastodon/reducers/relationships.js
@@ -11,9 +11,9 @@ import {
   DOMAIN_BLOCK_SUCCESS,
   DOMAIN_UNBLOCK_SUCCESS,
 } from '../actions/domain_blocks';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
-const normalizeRelationship = (state, relationship) => state.set(relationship.id, Immutable.fromJS(relationship));
+const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
 
 const normalizeRelationships = (state, relationships) => {
   relationships.forEach(relationship => {
@@ -23,7 +23,7 @@ const normalizeRelationships = (state, relationships) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function relationships(state = initialState, action) {
   switch(action.type) {
diff --git a/app/javascript/mastodon/reducers/reports.js b/app/javascript/mastodon/reducers/reports.js
index ad35eaa05..283c5b6f5 100644
--- a/app/javascript/mastodon/reducers/reports.js
+++ b/app/javascript/mastodon/reducers/reports.js
@@ -7,13 +7,13 @@ import {
   REPORT_STATUS_TOGGLE,
   REPORT_COMMENT_CHANGE,
 } from '../actions/reports';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
 
-const initialState = Immutable.Map({
-  new: Immutable.Map({
+const initialState = ImmutableMap({
+  new: ImmutableMap({
     isSubmitting: false,
     account_id: null,
-    status_ids: Immutable.Set(),
+    status_ids: ImmutableSet(),
     comment: '',
   }),
 });
@@ -26,14 +26,14 @@ export default function reports(state = initialState, action) {
       map.setIn(['new', 'account_id'], action.account.get('id'));
 
       if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
-        map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : Immutable.Set());
+        map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
         map.setIn(['new', 'comment'], '');
       } else {
-        map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
+        map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
       }
     });
   case REPORT_STATUS_TOGGLE:
-    return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => {
+    return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
       if (action.checked) {
         return set.add(action.statusId);
       }
@@ -50,7 +50,7 @@ export default function reports(state = initialState, action) {
   case REPORT_SUBMIT_SUCCESS:
     return state.withMutations(map => {
       map.setIn(['new', 'account_id'], null);
-      map.setIn(['new', 'status_ids'], Immutable.Set());
+      map.setIn(['new', 'status_ids'], ImmutableSet());
       map.setIn(['new', 'comment'], '');
       map.setIn(['new', 'isSubmitting'], false);
     });
diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js
index 0a3adac05..08d90e4e8 100644
--- a/app/javascript/mastodon/reducers/search.js
+++ b/app/javascript/mastodon/reducers/search.js
@@ -5,13 +5,13 @@ import {
   SEARCH_SHOW,
 } from '../actions/search';
 import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   value: '',
   submitted: false,
   hidden: false,
-  results: Immutable.Map(),
+  results: ImmutableMap(),
 });
 
 export default function search(state = initialState, action) {
@@ -21,7 +21,7 @@ export default function search(state = initialState, action) {
   case SEARCH_CLEAR:
     return state.withMutations(map => {
       map.set('value', '');
-      map.set('results', Immutable.Map());
+      map.set('results', ImmutableMap());
       map.set('submitted', false);
       map.set('hidden', false);
     });
@@ -31,10 +31,10 @@ export default function search(state = initialState, action) {
   case COMPOSE_MENTION:
     return state.set('hidden', true);
   case SEARCH_FETCH_SUCCESS:
-    return state.set('results', Immutable.Map({
-      accounts: Immutable.List(action.results.accounts.map(item => item.id)),
-      statuses: Immutable.List(action.results.statuses.map(item => item.id)),
-      hashtags: Immutable.List(action.results.hashtags),
+    return state.set('results', ImmutableMap({
+      accounts: ImmutableList(action.results.accounts.map(item => item.id)),
+      statuses: ImmutableList(action.results.statuses.map(item => item.id)),
+      hashtags: ImmutableList(action.results.hashtags),
     })).set('submitted', true);
   default:
     return state;
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 9a15a1fe3..1bdee7356 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -1,40 +1,40 @@
 import { SETTING_CHANGE } from '../actions/settings';
 import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 import uuid from '../uuid';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   onboarded: false,
   layout: 'auto',
 
-  home: Immutable.Map({
-    shows: Immutable.Map({
+  home: ImmutableMap({
+    shows: ImmutableMap({
       reblog: true,
       reply: true,
     }),
 
-    regex: Immutable.Map({
+    regex: ImmutableMap({
       body: '',
     }),
   }),
 
-  notifications: Immutable.Map({
-    alerts: Immutable.Map({
+  notifications: ImmutableMap({
+    alerts: ImmutableMap({
       follow: true,
       favourite: true,
       reblog: true,
       mention: true,
     }),
 
-    shows: Immutable.Map({
+    shows: ImmutableMap({
       follow: true,
       favourite: true,
       reblog: true,
       mention: true,
     }),
 
-    sounds: Immutable.Map({
+    sounds: ImmutableMap({
       follow: true,
       favourite: true,
       reblog: true,
@@ -42,20 +42,20 @@ const initialState = Immutable.Map({
     }),
   }),
 
-  community: Immutable.Map({
-    regex: Immutable.Map({
+  community: ImmutableMap({
+    regex: ImmutableMap({
       body: '',
     }),
   }),
 
-  public: Immutable.Map({
-    regex: Immutable.Map({
+  public: ImmutableMap({
+    regex: ImmutableMap({
       body: '',
     }),
   }),
 });
 
-const defaultColumns = Immutable.fromJS([
+const defaultColumns = fromJS([
   { id: 'COMPOSE', uuid: uuid(), params: {} },
   { id: 'HOME', uuid: uuid(), params: {} },
   { id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
@@ -83,7 +83,7 @@ export default function settings(state = initialState, action) {
   case SETTING_CHANGE:
     return state.setIn(action.key, action.value);
   case COLUMN_ADD:
-    return state.update('columns', list => list.push(Immutable.fromJS({ id: action.id, uuid: uuid(), params: action.params })));
+    return state.update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params })));
   case COLUMN_REMOVE:
     return state.update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid));
   case COLUMN_MOVE:
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index 7d00f6d30..bbc973302 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -2,13 +2,13 @@ import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
-  favourites: Immutable.Map({
+const initialState = ImmutableMap({
+  favourites: ImmutableMap({
     next: null,
     loaded: false,
-    items: Immutable.List(),
+    items: ImmutableList(),
   }),
 });
 
@@ -16,7 +16,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('items', Immutable.List(statuses.map(item => item.id)));
+    map.set('items', ImmutableList(statuses.map(item => item.id)));
   }));
 };
 
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 691135ff7..b1b1d0988 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -33,7 +33,7 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { SEARCH_FETCH_SUCCESS } from '../actions/search';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeStatus = (state, status) => {
   if (!status) {
@@ -51,7 +51,7 @@ const normalizeStatus = (state, status) => {
   const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
   normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent;
 
-  return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
+  return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
 };
 
 const normalizeStatuses = (state, statuses) => {
@@ -82,7 +82,7 @@ const filterStatuses = (state, relationship) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function statuses(state = initialState, action) {
   switch(action.type) {
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 1b738a16a..065e89f96 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -15,25 +15,25 @@ import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
 } from '../actions/accounts';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
-const initialTimeline = Immutable.Map({
+const initialTimeline = ImmutableMap({
   unread: 0,
   online: false,
   top: true,
   loaded: false,
   isLoading: false,
   next: false,
-  items: Immutable.List(),
+  items: ImmutableList(),
 });
 
 const normalizeTimeline = (state, timeline, statuses, next) => {
-  const ids       = Immutable.List(statuses.map(status => status.get('id')));
+  const ids       = ImmutableList(statuses.map(status => status.get('id')));
   const wasLoaded = state.getIn([timeline, 'loaded']);
   const hadNext   = state.getIn([timeline, 'next']);
-  const oldIds    = state.getIn([timeline, 'items'], Immutable.List());
+  const oldIds    = state.getIn([timeline, 'items'], ImmutableList());
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('loaded', true);
@@ -44,8 +44,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => {
 };
 
 const appendNormalizedTimeline = (state, timeline, statuses, next) => {
-  const ids    = Immutable.List(statuses.map(status => status.get('id')));
-  const oldIds = state.getIn([timeline, 'items'], Immutable.List());
+  const ids    = ImmutableList(statuses.map(status => status.get('id')));
+  const oldIds = state.getIn([timeline, 'items'], ImmutableList());
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('isLoading', false);
@@ -56,7 +56,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => {
 
 const updateTimeline = (state, timeline, status, references) => {
   const top        = state.getIn([timeline, 'top']);
-  const ids        = state.getIn([timeline, 'items'], Immutable.List());
+  const ids        = state.getIn([timeline, 'items'], ImmutableList());
   const includesId = ids.includes(status.get('id'));
   const unread     = state.getIn([timeline, 'unread'], 0);
 
@@ -124,11 +124,11 @@ export default function timelines(state = initialState, action) {
   case TIMELINE_EXPAND_FAIL:
     return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
   case TIMELINE_REFRESH_SUCCESS:
-    return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
+    return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next);
   case TIMELINE_EXPAND_SUCCESS:
-    return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
+    return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next);
   case TIMELINE_UPDATE:
-    return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
+    return updateTimeline(state, action.timeline, fromJS(action.status), action.references);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
   case ACCOUNT_BLOCK_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index 83bf1be1b..8db18c5dc 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -20,22 +20,22 @@ import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
 } from '../actions/mutes';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
-  followers: Immutable.Map(),
-  following: Immutable.Map(),
-  reblogged_by: Immutable.Map(),
-  favourited_by: Immutable.Map(),
-  follow_requests: Immutable.Map(),
-  blocks: Immutable.Map(),
-  mutes: Immutable.Map(),
+const initialState = ImmutableMap({
+  followers: ImmutableMap(),
+  following: ImmutableMap(),
+  reblogged_by: ImmutableMap(),
+  favourited_by: ImmutableMap(),
+  follow_requests: ImmutableMap(),
+  blocks: ImmutableMap(),
+  mutes: ImmutableMap(),
 });
 
 const normalizeList = (state, type, id, accounts, next) => {
-  return state.setIn([type, id], Immutable.Map({
+  return state.setIn([type, id], ImmutableMap({
     next,
-    items: Immutable.List(accounts.map(item => item.id)),
+    items: ImmutableList(accounts.map(item => item.id)),
   }));
 };
 
@@ -56,22 +56,22 @@ export default function userLists(state = initialState, action) {
   case FOLLOWING_EXPAND_SUCCESS:
     return appendToList(state, 'following', action.id, action.accounts, action.next);
   case REBLOGS_FETCH_SUCCESS:
-    return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
+    return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FAVOURITES_FETCH_SUCCESS:
-    return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
+    return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FOLLOW_REQUESTS_FETCH_SUCCESS:
-    return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
+    return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
   case FOLLOW_REQUESTS_EXPAND_SUCCESS:
     return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
   case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
   case FOLLOW_REQUEST_REJECT_SUCCESS:
     return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
   case BLOCKS_FETCH_SUCCESS:
-    return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
+    return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
   case BLOCKS_EXPAND_SUCCESS:
     return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
   case MUTES_FETCH_SUCCESS:
-    return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
+    return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
   case MUTES_EXPAND_SUCCESS:
     return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
   default:
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 07d9a2629..d26d1b727 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -1,5 +1,5 @@
 import { createSelector } from 'reselect';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 
 const getAccountBase         = (state, id) => state.getIn(['accounts', id], null);
 const getAccountCounters     = (state, id) => state.getIn(['accounts_counters', id], null);
@@ -73,10 +73,10 @@ export const makeGetNotification = () => {
 };
 
 export const getAccountGallery = createSelector([
-  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], Immutable.List()),
+  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
   state       => state.get('statuses'),
 ], (statusIds, statuses) => {
-  let medias = Immutable.List();
+  let medias = ImmutableList();
 
   statusIds.forEach(statusId => {
     const status = statuses.get(statusId);
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
index 9d63d8f98..a0cb91ae4 100644
--- a/app/javascript/packs/common.js
+++ b/app/javascript/packs/common.js
@@ -1,2 +1,7 @@
 import { start } from 'rails-ujs';
+
+// import default stylesheet with variables
+require('font-awesome/css/font-awesome.css');
+require('mastodon-application-style');
+
 start();
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 8a3ae0b3c..06cc1b53a 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -5,6 +5,9 @@ import emojify from '../mastodon/emoji';
 import { getLocale } from '../mastodon/locales';
 import loadPolyfills from '../mastodon/load_polyfills';
 import { processBio } from '../glitch/util/bio_metadata';
+import TimelineContainer from '../mastodon/containers/timeline_container';
+import React from 'react';
+import ReactDOM from 'react-dom';
 
 require.context('../images/', true);
 
@@ -37,6 +40,13 @@ function loaded() {
     const datetime = new Date(content.getAttribute('datetime'));
     content.textContent = relativeFormat.format(datetime);;
   });
+
+  const mountNode = document.getElementById('mastodon-timeline');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<TimelineContainer {...props} />, mountNode);
+  }
 }
 
 function main() {
@@ -54,8 +64,12 @@ function main() {
     }
   });
 
-  delegate(document, '.media-spoiler', 'click', ({ target }) => {
-    target.style.display = 'none';
+  delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() {
+    this.parentNode.classList.add('media-spoiler-wrapper__visible');
+  });
+
+  delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() {
+    this.parentNode.classList.remove('media-spoiler-wrapper__visible');
   });
 
   delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss
index 7145d0092..5716163be 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/about.scss
@@ -116,10 +116,6 @@
     .wrapper {
       padding: 20px;
     }
-
-    .features-list {
-      display: block;
-    }
   }
 }
 
@@ -299,80 +295,438 @@
   }
 }
 
-.features-list {
+.features-list__row {
   display: flex;
-  margin-bottom: 20px;
+  padding: 10px 0;
+  justify-content: space-between;
+
+  &:first-child {
+    padding-top: 0;
+  }
 
-  .features-list__column {
-    flex: 1 1 0;
+  .visual {
+    flex: 0 0 auto;
+    display: flex;
+    align-items: center;
+    margin-left: 15px;
 
-    ul {
-      list-style: none;
+    .fa {
+      display: block;
+      color: $ui-primary-color;
+      font-size: 48px;
     }
+  }
 
-    li {
-      margin: 0;
+  .text {
+    font-size: 16px;
+    line-height: 30px;
+    color: lighten($ui-base-color, 26%);
+
+    h6 {
+      font-weight: 500;
+      color: $ui-primary-color;
     }
   }
 }
 
-.screenshot-with-signup {
-  display: flex;
-  margin-bottom: 20px;
-
-  .mascot {
-    flex: 1 1 auto;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-direction: column;
+.landing-page {
+  .header-wrapper {
+    padding-top: 15px;
+    background: $ui-base-color;
+    background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color);
+    position: relative;
 
-    img {
-      display: block;
+    .mascot-container {
+      max-width: 800px;
       margin: 0 auto;
-      max-width: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      height: 100%;
+    }
+
+    .mascot {
+      position: absolute;
+      bottom: -14px;
+      width: auto;
       height: auto;
+      left: 60px;
+      z-index: 3;
+    }
+  }
+
+  p,
+  li {
+    font: inherit;
+    font-weight: inherit;
+    margin-bottom: 0;
+  }
+
+  .header {
+    line-height: 30px;
+    overflow: hidden;
+
+    .container {
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .hero {
+      margin-top: 50px;
+      align-items: center;
+      position: relative;
+
+      .floats {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 0;
+        left: 0;
+
+        img {
+          position: absolute;
+          transition: all 0.1s linear;
+          animation-name: floating;
+          animation-duration: 1.7s;
+          animation-iteration-count: infinite;
+          animation-direction: alternate;
+          animation-timing-function: linear;
+          z-index: 2;
+        }
+
+        .float-1 {
+          height: 170px;
+          right: -120px;
+          bottom: 0;
+        }
+
+        .float-2 {
+          height: 100px;
+          right: 210px;
+          bottom: 0;
+          animation-delay: 0.2s;
+        }
+
+        .float-3 {
+          height: 140px;
+          right: 110px;
+          top: -30px;
+          animation-delay: 0.1s;
+        }
+      }
+
+      .simple_form,
+      .closed-registrations-message {
+        background: darken($ui-base-color, 4%);
+        width: 280px;
+        padding: 15px 20px;
+        border-radius: 4px 4px 0 0;
+        line-height: initial;
+        position: relative;
+        z-index: 4;
+
+        .actions {
+          margin-bottom: 0;
+
+          button,
+          .button,
+          .block-button {
+            margin-bottom: 0;
+          }
+        }
+      }
+
+      .heading {
+        position: relative;
+        z-index: 4;
+        padding-bottom: 150px;
+      }
+
+      .closed-registrations-message {
+        min-height: 330px;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+      }
+    }
+
+    ul {
+      list-style: none;
+      margin: 0;
+
+      li {
+        display: inline-block;
+        vertical-align: bottom;
+        margin: 0;
+
+        &:first-child a {
+          padding-left: 0;
+        }
+
+        &:last-child a {
+          padding-right: 0;
+        }
+      }
+    }
+
+    .links {
+      position: relative;
+      z-index: 4;
+
+      a {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        color: $ui-primary-color;
+        text-decoration: none;
+        padding: 12px 16px;
+        line-height: 32px;
+        font-family: 'mastodon-font-display', sans-serif;
+        font-weight: 500;
+        font-size: 14px;
+
+        &:hover {
+          color: $ui-secondary-color;
+        }
+      }
+
+      .brand {
+        a {
+          padding-left: 0;
+          color: $white;
+        }
+
+        img {
+          width: 32px;
+          height: 32px;
+          margin-right: 10px;
+        }
+      }
+    }
+  }
+
+  .container {
+    width: 100%;
+    box-sizing: border-box;
+    max-width: 800px;
+    margin: 0 auto;
+  }
+
+  .wrapper {
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 0;
+  }
+
+  .learn-more-cta {
+    background: darken($ui-base-color, 4%);
+    padding: 50px 0;
+  }
+
+  h3 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 16px;
+    line-height: 24px;
+    font-weight: 500;
+    margin-bottom: 20px;
+    color: $ui-primary-color;
+  }
+
+  p {
+    font-size: 16px;
+    line-height: 30px;
+    color: lighten($ui-base-color, 26%);
+  }
+
+  .features {
+    padding: 50px 0;
+
+    .container {
+      display: flex;
     }
   }
 
-  .simple_form,
-  .closed-registrations-message {
-    width: 300px;
+  #mastodon-timeline {
+    -webkit-overflow-scrolling: touch;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    font-family: 'mastodon-font-sans-serif', sans-serif;
+    font-size: 13px;
+    line-height: 18px;
+    font-weight: 400;
+    color: $primary-text-color;
+    width: 330px;
+    margin-right: 30px;
     flex: 0 0 auto;
-    background: rgba(darken($ui-base-color, 7%), 0.5);
-    padding: 14px;
-    border-radius: 4px;
-    box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
+    background: $ui-base-color;
+    overflow: hidden;
+    box-shadow: 0 0 6px rgba($black, 0.1);
 
-    .actions {
-      margin-bottom: 0;
+    .column {
+      padding: 0;
+      border-radius: 4px;
+      overflow: hidden;
+      height: 100%;
     }
 
-    .info {
-      text-align: center;
+    .scrollable {
+      height: 400px;
+    }
+
+    p {
+      font-size: inherit;
+      line-height: inherit;
+      font-weight: inherit;
+      color: $primary-text-color;
 
       a {
         color: $ui-secondary-color;
+        text-decoration: none;
       }
     }
   }
 
-  @media screen and (max-width: 625px) {
-    .mascot {
+  .about-mastodon {
+    max-width: 675px;
+
+    p {
+      margin-bottom: 20px;
+    }
+
+    .features-list {
+      margin-top: 20px;
+    }
+  }
+
+  em {
+    display: inline;
+    margin: 0;
+    padding: 0;
+    font-weight: 500;
+    background: transparent;
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    color: $ui-primary-color;
+  }
+
+  h1 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 26px;
+    line-height: 30px;
+    margin-bottom: 0;
+    font-weight: 500;
+    color: $ui-secondary-color;
+
+    small {
+      font-family: 'mastodon-font-sans-serif', sans-serif;
+      display: block;
+      font-size: 18px;
+      font-weight: 400;
+      color: lighten($ui-base-color, 26%);
+    }
+  }
+
+  .footer-links {
+    padding-bottom: 50px;
+    text-align: right;
+    color: lighten($ui-base-color, 26%);
+
+    p {
+      font-size: 14px;
+    }
+
+    a {
+      color: inherit;
+      text-decoration: underline;
+    }
+  }
+
+  @media screen and (max-width: 800px) {
+    .container {
+      padding: 0 20px;
+    }
+
+    .header-wrapper .mascot {
+      left: 20px;
+    }
+  }
+
+  @media screen and (max-width: 689px) {
+    .header-wrapper .mascot {
       display: none;
     }
+  }
 
-    .simple_form,
-    .closed-registrations-message {
-      flex: auto;
+  @media screen and (max-width: 675px) {
+    .header-wrapper {
+      padding-top: 0;
+    }
+
+    .header .container,
+    .features .container {
+      display: block;
+    }
+
+    .links {
+      padding-top: 15px;
+      background: darken($ui-base-color, 4%);
+    }
+
+    .header {
+      padding-top: 0;
+
+      .hero {
+        margin-top: 30px;
+        padding: 0;
+
+        .heading {
+          padding-bottom: 20px;
+        }
+      }
+
+      .floats {
+        display: none;
+      }
+
+      .heading,
+      .nav {
+        text-align: center;
+      }
+
+      .heading h1 {
+        padding: 30px 0;
+      }
+
+      .hero {
+        .simple_form,
+        .closed-registrations-message {
+          background: darken($ui-base-color, 8%);
+          width: 100%;
+          border-radius: 0;
+          box-sizing: border-box;
+        }
+      }
+    }
+
+    #mastodon-timeline {
+      height: 70vh;
+      width: 100%;
+      margin-bottom: 50px;
     }
   }
 }
 
-.closed-registrations-message {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  text-align: center;
+@keyframes floating {
+  from {
+    transform: translate(0, 0);
+  }
+
+  65% {
+    transform: translate(0, 4px);
+  }
+
+  to {
+    transform: translate(0, -0);
+  }
 }
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss
index 70a5be367..182ea36a4 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/basics.scss
@@ -1,6 +1,6 @@
 body {
   font-family: 'mastodon-font-sans-serif', sans-serif;
-  background: $ui-base-color url('../images/background-photo.jpg');
+  background: $ui-base-color;
   background-size: cover;
   background-attachment: fixed;
   font-size: 13px;
@@ -11,6 +11,8 @@ body {
   text-rendering: optimizelegibility;
   font-feature-settings: "kern";
   text-size-adjust: none;
+  -webkit-tap-highlight-color: rgba(0,0,0,0);
+  -webkit-tap-highlight-color: transparent;
 
   &.app-body {
     position: fixed;
@@ -20,6 +22,11 @@ body {
     background: $ui-base-color;
   }
 
+  &.about-body {
+    background: darken($ui-base-color, 8%);
+    padding-bottom: 0;
+  }
+
   &.embed {
     background: transparent;
     margin: 0;
@@ -61,3 +68,18 @@ button {
   align-items: center;
   justify-content: center;
 }
+
+.system-font {
+  // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
+  // -apple-system => Safari <11 specific
+  // BlinkMacSystemFont => Chrome <56 on macOS specific
+  // Segoe UI => Windows 7/8/10
+  // Oxygen => KDE
+  // Ubuntu => Unity/Ubuntu
+  // Cantarell => GNOME
+  // Fira Sans => Firefox OS
+  // Droid Sans => Older Androids (<4.0)
+  // Helvetica Neue => Older macOS <10.11
+  // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
+  font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif;
+}
diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss
index 9cad7a4f5..e44df2ea4 100644
--- a/app/javascript/styles/boost.scss
+++ b/app/javascript/styles/boost.scss
@@ -1,12 +1,15 @@
-@function url-friendly-colour($colour) {
-  @return '%23' + str-slice('#{$colour}', 2, -1)
+@function hex-color($color) {
+  @if type-of($color) == 'color' {
+    $color: str-slice(ie-hex-str($color), 4);
+  }
+  @return '%23' + unquote($color)
 }
 
 button.icon-button i.fa-retweet {
-  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($ui-base-color, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($ui-highlight-color)}' stroke-width='0'/></svg>");
+  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>");
 
   &:hover {
-    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($ui-highlight-color)}' stroke-width='0'/></svg>");
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>");
   }
 }
 
@@ -23,3 +26,11 @@ button.icon-button.disabled i.fa-retweet {
     background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{lighten($ui-base-color, 16%)}' stroke-width='0'/></svg>");
   }
 }
+
+//  Mastodon gave us this one, but I'm not sure if it's better.  - @kibi@glitch.social
+
+/*
+button.icon-button.disabled i.fa-retweet {
+  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 13%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>");
+}
+*/
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 49d3c9873..6cca3666a 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -42,8 +42,38 @@
     cursor: default;
   }
 
+  &.button-alternative {
+    font-size: 16px;
+    line-height: 36px;
+    height: auto;
+    color: $ui-base-color;
+    background: $ui-primary-color;
+    text-transform: none;
+    padding: 4px 16px;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: lighten($ui-primary-color, 4%);
+    }
+  }
+
   &.button-secondary {
-    //
+    font-size: 16px;
+    line-height: 36px;
+    height: auto;
+    color: $ui-primary-color;
+    text-transform: none;
+    background: transparent;
+    padding: 3px 15px;
+    border: 1px solid $ui-primary-color;
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: lighten($ui-primary-color, 4%);
+      color: lighten($ui-primary-color, 4%);
+    }
   }
 
   &.button--block {
@@ -1327,6 +1357,20 @@
     height: 100%;
     background-image: none;
   }
+
+  &.image-loader--amorphous {
+    position: static;
+
+    .image-loader__preview-canvas {
+      display: none;
+    }
+
+    .image-loader__img {
+      position: static;
+      width: auto;
+      height: auto;
+    }
+  }
 }
 
 .navigation-bar {
@@ -1463,6 +1507,23 @@
   .columns-area {
     padding: 0;
   }
+
+  .react-swipeable-view-container .columns-area {
+    height: calc(100% - 20px) !important;
+  }
+}
+
+.react-swipeable-view-container {
+  &,
+  .columns-area,
+  .drawer,
+  .column {
+    height: 100%;
+  }
+}
+
+.react-swipeable-view-container > * {
+  height: 100%;
 }
 
 .column {
@@ -1510,8 +1571,7 @@
 .drawer__tab {
   display: block;
   flex: 1 1 auto;
-  padding: 15px;
-  padding-bottom: 13px;
+  padding: 15px 5px 13px;
   color: $ui-primary-color;
   text-decoration: none;
   text-align: center;
@@ -2537,7 +2597,8 @@ button.icon-button.active i.fa-retweet {
   vertical-align: middle;
 }
 
-.empty-column-indicator {
+.empty-column-indicator,
+.error-column {
   color: lighten($ui-base-color, 20%);
   background: $ui-base-color;
   text-align: center;
@@ -2563,6 +2624,10 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.error-column {
+  flex-direction: column;
+}
+
 @keyframes pulse {
   0% {
     opacity: 1;
@@ -3206,7 +3271,7 @@ button.icon-button.active i.fa-retweet {
   video {
     max-width: 80vw;
     max-height: 80vh;
-    width: auto;
+    width: 100%;
     height: auto;
   }
 
@@ -3214,6 +3279,7 @@ button.icon-button.active i.fa-retweet {
   canvas {
     display: block;
     background: url('../images/void.png') repeat;
+    object-fit: contain;
   }
 }
 
@@ -3224,7 +3290,8 @@ button.icon-button.active i.fa-retweet {
   z-index: 100;
 }
 
-.onboarding-modal {
+.onboarding-modal,
+.error-modal {
   background: $ui-secondary-color;
   color: $ui-base-color;
   border-radius: 8px;
@@ -3238,6 +3305,26 @@ button.icon-button.active i.fa-retweet {
   width: 80vw;
   max-width: 520px;
   max-height: 420px;
+
+  .react-swipeable-view-container > div {
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    padding: 25px;
+    display: none;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+    user-select: text;
+  }
+}
+
+.error-modal__body {
+  height: 80vh;
+  width: 80vw;
+  max-width: 520px;
+  max-height: 420px;
   position: relative;
 
   & > div {
@@ -3258,6 +3345,14 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.error-modal__body {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  text-align: center;
+}
+
 @media screen and (max-width: 550px) {
   .onboarding-modal {
     width: 100%;
@@ -3274,7 +3369,8 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
-.onboarding-modal__paginator {
+.onboarding-modal__paginator,
+.error-modal__footer {
   flex: 0 0 auto;
   background: darken($ui-secondary-color, 8%);
   display: flex;
@@ -3284,7 +3380,8 @@ button.icon-button.active i.fa-retweet {
     min-width: 33px;
   }
 
-  .onboarding-modal__nav {
+  .onboarding-modal__nav,
+  .error-modal__nav {
     color: darken($ui-secondary-color, 34%);
     background-color: transparent;
     border: 0;
@@ -3307,6 +3404,10 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.error-modal__footer {
+  justify-content: center;
+}
+
 .onboarding-modal__dots {
   flex: 1 1 auto;
   display: flex;
@@ -3676,6 +3777,7 @@ button.icon-button.active i.fa-retweet {
 
 .report-modal__statuses {
   min-height: 20vh;
+  max-height: 40vh;
   overflow-y: auto;
   overflow-x: hidden;
 }
@@ -3831,8 +3933,7 @@ button.icon-button.active i.fa-retweet {
 
 .media-gallery__item-thumbnail {
   cursor: zoom-in;
-  display: flex;
-  align-items: center;
+  display: block;
   text-decoration: none;
   width: 100%;
   height: 100%;
diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss
index 68f73e0c0..44d4c1118 100644
--- a/app/javascript/styles/containers.scss
+++ b/app/javascript/styles/containers.scss
@@ -10,52 +10,36 @@
 }
 
 .logo-container {
-  max-width: 400px;
   margin: 100px auto;
-  margin-bottom: 0;
-  cursor: default;
+  margin-bottom: 50px;
 
   @media screen and (max-width: 360px) {
     margin: 30px auto;
   }
 
   h1 {
-    display: block;
-    text-align: center;
-    color: $primary-text-color;
-    font-size: 48px;
-    font-weight: 500;
+    display: flex;
+    justify-content: center;
+    align-items: center;
 
     img {
-      display: block;
-      margin: 20px auto;
-      width: 180px;
-      height: 180px;
+      width: 32px;
+      height: 32px;
+      margin-right: 10px;
     }
 
     a {
-      color: inherit;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: $primary-text-color;
       text-decoration: none;
       outline: 0;
-
-      img {
-        opacity: 0.8;
-        transition: opacity 0.8s ease;
-      }
-
-      &:hover {
-        img {
-          opacity: 1;
-          transition-duration: 0.2s;
-        }
-      }
-    }
-
-    small {
-      display: block;
-      font-size: 12px;
-      font-weight: 400;
-      font-family: 'mastodon-font-monospace', monospace;
+      padding: 12px 16px;
+      line-height: 32px;
+      font-family: 'mastodon-font-display', sans-serif;
+      font-weight: 500;
+      font-size: 14px;
     }
   }
 }
diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss
index e4012ab02..206f1865e 100644
--- a/app/javascript/styles/fonts/montserrat.scss
+++ b/app/javascript/styles/fonts/montserrat.scss
@@ -7,3 +7,11 @@
   font-weight: 400;
   font-style: normal;
 }
+
+@font-face {
+  font-family: 'mastodon-font-display';
+  src: local('Montserrat'),
+    url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
+  font-weight: 500;
+  font-style: normal;
+}
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index 7a181f36b..e1de36d55 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -24,6 +24,20 @@ code {
 
   p.hint {
     margin-bottom: 15px;
+    color: lighten($ui-base-color, 32%);
+
+    &.subtle-hint {
+      text-align: center;
+      font-size: 12px;
+      line-height: 18px;
+      margin-top: 15px;
+      margin-bottom: 0;
+      color: lighten($ui-base-color, 26%);
+
+      a {
+        color: $ui-primary-color;
+      }
+    }
   }
 
   strong {
@@ -43,10 +57,7 @@ code {
     }
   }
 
-  .input.file,
-  .input.select,
-  .input.radio_buttons,
-  .input.check_boxes {
+  .input.with_label {
     padding: 15px 0;
     margin-bottom: 0;
 
@@ -57,6 +68,44 @@ code {
       display: block;
       padding-top: 5px;
     }
+
+    &.boolean {
+      padding: initial;
+      margin-bottom: initial;
+
+      .label_input > label {
+        font-family: inherit;
+        font-size: 14px;
+        color: $primary-text-color;
+        display: block;
+        width: auto;
+      }
+
+      label.checkbox {
+        position: relative;
+        padding-left: 25px;
+        flex: 1 1 auto;
+      }
+    }
+  }
+
+  .input.with_block_label {
+    & > label {
+      font-family: inherit;
+      font-size: 16px;
+      color: $primary-text-color;
+      display: block;
+      padding-top: 5px;
+    }
+
+    .hint {
+      margin-bottom: 15px;
+    }
+
+    li {
+      float: left;
+      width: 50%;
+    }
   }
 
   .fields-group {
@@ -92,7 +141,7 @@ code {
     input[type=checkbox] {
       position: absolute;
       left: 0;
-      top: 1px;
+      top: 5px;
       margin: 0;
     }
 
@@ -102,6 +151,29 @@ code {
     }
   }
 
+  .check_boxes {
+    .checkbox {
+      label {
+        font-family: inherit;
+        font-size: 14px;
+        color: $primary-text-color;
+        display: block;
+        width: auto;
+        position: relative;
+        padding-top: 5px;
+        padding-left: 25px;
+        flex: 1 1 auto;
+      }
+
+      input[type=checkbox] {
+        position: absolute;
+        left: 0;
+        top: 5px;
+        margin: 0;
+      }
+    }
+  }
+
   input[type=text],
   input[type=number],
   input[type=email],
@@ -197,8 +269,6 @@ code {
 
     &:active,
     &:focus {
-      position: relative;
-      top: 1px;
       background-color: darken($ui-highlight-color, 5%);
     }
 
@@ -219,6 +289,27 @@ code {
   select {
     font-size: 16px;
   }
+
+  .input-with-append {
+    position: relative;
+
+    .input input {
+      padding-right: 127px;
+    }
+
+    .append {
+      position: absolute;
+      right: 0;
+      top: 0;
+      padding: 7px 4px;
+      padding-bottom: 9px;
+      font-size: 16px;
+      color: lighten($ui-base-color, 26%);
+      font-family: inherit;
+      pointer-events: none;
+      cursor: default;
+    }
+  }
 }
 
 .flash-message {
@@ -240,7 +331,7 @@ code {
   text-align: center;
 
   a {
-    color: $primary-text-color;
+    color: $ui-primary-color;
     text-decoration: none;
 
     &:hover {
@@ -357,21 +448,11 @@ code {
   }
 }
 
-.user_filtered_languages {
-  & > label {
-    font-family: inherit;
-    font-size: 16px;
-    color: $primary-text-color;
-    display: block;
-    padding-top: 5px;
-  }
-
-  .hint {
-    margin-bottom: 15px;
-  }
+.post-follow-actions {
+  text-align: center;
+  color: $ui-primary-color;
 
-  li {
-    float: left;
-    width: 50%;
+  div {
+    margin-bottom: 4px;
   }
 }
diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss
index 8ecf31fd5..a9111d7c9 100644
--- a/app/javascript/styles/stream_entries.scss
+++ b/app/javascript/styles/stream_entries.scss
@@ -326,6 +326,18 @@
     }
   }
 
+  .media-spoiler-wrapper {
+    &.media-spoiler-wrapper__visible {
+      .media-spoiler {
+        display: none;
+      }
+
+      .spoiler-button {
+        display: block;
+      }
+    }
+  }
+
   .pre-header {
     padding: 14px 0;
     padding-left: (48px + 14px * 2);
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 1885eff26..35b18fa1b 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -38,9 +38,7 @@ class FeedManager
   end
 
   def trim(type, account_id)
-    return unless redis.zcard(key(type, account_id)) > FeedManager::MAX_ITEMS
-    last = redis.zrevrange(key(type, account_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1)
-    redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
+    redis.zremrangebyrank(key(type, account_id), '0', (-(FeedManager::MAX_ITEMS + 1)).to_s)
   end
 
   def push_update_required?(timeline_type, account_id)
diff --git a/app/lib/inline_rabl_scope.rb b/app/lib/inline_rabl_scope.rb
deleted file mode 100644
index 26adcb03a..000000000
--- a/app/lib/inline_rabl_scope.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-class InlineRablScope
-  include RoutingHelper
-
-  def initialize(account)
-    @account = account
-  end
-
-  def current_user
-    @account.try(:user)
-  end
-
-  def current_account
-    @account
-  end
-end
diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb
index 8e04ad1d5..7cd9758ec 100644
--- a/app/lib/inline_renderer.rb
+++ b/app/lib/inline_renderer.rb
@@ -1,13 +1,33 @@
 # frozen_string_literal: true
 
 class InlineRenderer
-  def self.render(status, current_account, template)
-    Rabl::Renderer.new(
-      template,
-      status,
-      view_path: 'app/views',
-      format: :json,
-      scope: InlineRablScope.new(current_account)
-    ).render
+  def initialize(object, current_account, template)
+    @object          = object
+    @current_account = current_account
+    @template        = template
+  end
+
+  def render
+    case @template
+    when :status
+      serializer = REST::StatusSerializer
+    when :notification
+      serializer = REST::NotificationSerializer
+    else
+      return
+    end
+
+    serializable_resource = ActiveModelSerializers::SerializableResource.new(@object, serializer: serializer, scope: current_user, scope_name: :current_user)
+    serializable_resource.as_json
+  end
+
+  def self.render(object, current_account, template)
+    new(object, current_account, template).render
+  end
+
+  private
+
+  def current_user
+    @current_account&.user
   end
 end
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index af264bbd5..e0e92b19d 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -18,9 +18,11 @@ class UserSettingsDecorator
     user.settings['notification_emails'] = merged_notification_emails
     user.settings['interactions'] = merged_interactions
     user.settings['default_privacy'] = default_privacy_preference
+    user.settings['default_sensitive'] = default_sensitive_preference
     user.settings['boost_modal'] = boost_modal_preference
     user.settings['delete_modal'] = delete_modal_preference
     user.settings['auto_play_gif'] = auto_play_gif_preference
+    user.settings['system_font_ui'] = system_font_ui_preference
   end
 
   def merged_notification_emails
@@ -35,6 +37,10 @@ class UserSettingsDecorator
     settings['setting_default_privacy']
   end
 
+  def default_sensitive_preference
+    boolean_cast_setting 'setting_default_sensitive'
+  end
+
   def boost_modal_preference
     boolean_cast_setting 'setting_boost_modal'
   end
@@ -43,6 +49,10 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_delete_modal'
   end
 
+  def system_font_ui_preference
+    boolean_cast_setting 'setting_system_font_ui'
+  end
+
   def auto_play_gif_preference
     boolean_cast_setting 'setting_auto_play_gif'
   end
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index fc19a6d40..fd9223533 100644
--- a/app/mailers/admin_mailer.rb
+++ b/app/mailers/admin_mailer.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class AdminMailer < ApplicationMailer
+  helper StreamEntriesHelper
+
   def new_report(recipient, report)
     @report   = report
     @me       = recipient
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 2e730c19b..95b770ff1 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 class ApplicationMailer < ActionMailer::Base
-  default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
   layout 'mailer'
   helper :instance
 
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 6abf9c9ca..1517c027e 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 class UserMailer < Devise::Mailer
-  default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
   layout 'mailer'
 
   helper :instance
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 4a412ee3d..b4f169649 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -6,11 +6,16 @@ module Remotable
 
   included do
     attachment_definitions.each_key do |attachment_name|
-      attribute_name = "#{attachment_name}_remote_url".to_sym
-      method_name = "#{attribute_name}=".to_sym
+      attribute_name  = "#{attachment_name}_remote_url".to_sym
+      method_name     = "#{attribute_name}=".to_sym
+      alt_method_name = "reset_#{attachment_name}!".to_sym
 
       define_method method_name do |url|
-        parsed_url = Addressable::URI.parse(url).normalize
+        begin
+          parsed_url = Addressable::URI.parse(url).normalize
+        rescue Addressable::URI::InvalidURIError
+          return
+        end
 
         return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url
 
@@ -26,10 +31,20 @@ module Remotable
           send("#{attachment_name}_file_name=", filename)
 
           self[attribute_name] = url if has_attribute?(attribute_name)
-        rescue HTTP::TimeoutError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError => e
+        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e
           Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
+          nil
         end
       end
+
+      define_method alt_method_name do
+        url = self[attribute_name]
+
+        return if url.blank?
+
+        self[attribute_name] = ''
+        send(method_name, url)
+      end
     end
   end
 end
diff --git a/app/models/context.rb b/app/models/context.rb
new file mode 100644
index 000000000..cc667999e
--- /dev/null
+++ b/app/models/context.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class Context < ActiveModelSerializers::Model
+  attributes :ancestors, :descendants
+end
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 5125e51ff..beb4a8de3 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -20,8 +20,7 @@ class Feed
     max_id     = '+inf' if max_id.blank?
     since_id   = '-inf' if since_id.blank?
     unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)
-    status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
-    unhydrated.map { |id| status_map[id] }.compact
+    Status.where(id: unhydrated).cache_ids
   end
 
   def from_database(limit, max_id, since_id)
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
new file mode 100644
index 000000000..c3a04ba65
--- /dev/null
+++ b/app/models/form/admin_settings.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Form::AdminSettings
+  include ActiveModel::Model
+
+  delegate(
+    :site_contact_username,
+    :site_contact_username=,
+    :site_contact_email,
+    :site_contact_email=,
+    :site_title,
+    :site_title=,
+    :site_description,
+    :site_description=,
+    :site_extended_description,
+    :site_extended_description=,
+    :site_terms,
+    :site_terms=,
+    :open_registrations,
+    :open_registrations=,
+    :closed_registrations_message,
+    :closed_registrations_message=,
+    :open_deletion,
+    :open_deletion=,
+    :timeline_preview,
+    :timeline_preview=,
+    to: Setting
+  )
+end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 340109ab6..1e8c6d00a 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -18,6 +18,8 @@
 #  file_meta         :json
 #
 
+require 'mime/types'
+
 class MediaAttachment < ApplicationRecord
   self.inheritance_column = nil
 
diff --git a/app/models/search.rb b/app/models/search.rb
new file mode 100644
index 000000000..676c2a7f8
--- /dev/null
+++ b/app/models/search.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class Search < ActiveModelSerializers::Model
+  attributes :accounts, :statuses, :hashtags
+end
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index 02a918e8a..887e3e3bd 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -69,9 +69,7 @@ class SessionActivation < ApplicationRecord
   def assign_access_token
     superapp = Doorkeeper::Application.find_by(superapp: true)
 
-    return if superapp.nil?
-
-    self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp.id,
+    self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp&.id,
                                                         resource_owner_id: user_id,
                                                         scopes: 'read write follow',
                                                         expires_in: Doorkeeper.configuration.access_token_expires_in,
diff --git a/app/models/user.rb b/app/models/user.rb
index c31a0c644..c80115a08 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -79,6 +79,10 @@ class User < ApplicationRecord
     settings.default_privacy || (account.locked? ? 'private' : 'public')
   end
 
+  def setting_default_sensitive
+    settings.default_sensitive
+  end
+
   def setting_boost_modal
     settings.boost_modal
   end
@@ -91,6 +95,10 @@ class User < ApplicationRecord
     settings.auto_play_gif
   end
 
+  def setting_system_font_ui
+    settings.system_font_ui
+  end
+
   def activate_session(request)
     session_activations.activate(session_id: SecureRandom.hex,
                                  user_agent: request.user_agent,
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
new file mode 100644
index 000000000..657807863
--- /dev/null
+++ b/app/presenters/account_relationships_presenter.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AccountRelationshipsPresenter
+  attr_reader :following, :followed_by, :blocking,
+              :muting, :requested, :domain_blocking
+
+  def initialize(account_ids, current_account_id)
+    @following       = Account.following_map(account_ids, current_account_id)
+    @followed_by     = Account.followed_by_map(account_ids, current_account_id)
+    @blocking        = Account.blocking_map(account_ids, current_account_id)
+    @muting          = Account.muting_map(account_ids, current_account_id)
+    @requested       = Account.requested_map(account_ids, current_account_id)
+    @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id)
+  end
+end
diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb
new file mode 100644
index 000000000..75fef28a8
--- /dev/null
+++ b/app/presenters/initial_state_presenter.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class InitialStatePresenter < ActiveModelSerializers::Model
+  attributes :settings, :token, :current_account, :admin
+end
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index c96eaa1cb..19bedcc21 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -5,8 +5,10 @@ class InstancePresenter
     :closed_registrations_message,
     :site_contact_email,
     :open_registrations,
+    :site_title,
     :site_description,
     :site_extended_description,
+    :site_terms,
     to: Setting
   )
 
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
new file mode 100644
index 000000000..caf00791a
--- /dev/null
+++ b/app/presenters/status_relationships_presenter.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class StatusRelationshipsPresenter
+  attr_reader :reblogs_map, :favourites_map, :mutes_map
+
+  def initialize(statuses, current_account_id = nil)
+    if current_account_id.nil?
+      @reblogs_map    = {}
+      @favourites_map = {}
+      @mutes_map      = {}
+    else
+      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_id)
+      @favourites_map  = Status.favourites_map(status_ids, current_account_id)
+      @mutes_map       = Status.mutes_map(conversation_ids, current_account_id)
+    end
+  end
+end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
new file mode 100644
index 000000000..6751c9411
--- /dev/null
+++ b/app/serializers/initial_state_serializer.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class InitialStateSerializer < ActiveModel::Serializer
+  attributes :meta, :compose, :accounts,
+             :media_attachments, :settings
+
+  def meta
+    store = {
+      streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
+      access_token: object.token,
+      locale: I18n.locale,
+      domain: Rails.configuration.x.local_domain,
+      admin: object.admin&.id,
+    }
+
+    if object.current_account
+      store[:me]             = object.current_account.id
+      store[:boost_modal]    = object.current_account.user.setting_boost_modal
+      store[:delete_modal]   = object.current_account.user.setting_delete_modal
+      store[:auto_play_gif]  = object.current_account.user.setting_auto_play_gif
+      store[:system_font_ui] = object.current_account.user.setting_system_font_ui
+    end
+
+    store
+  end
+
+  def compose
+    store = {}
+
+    if object.current_account
+      store[:me]                = object.current_account.id
+      store[:default_privacy]   = object.current_account.user.setting_default_privacy
+      store[:default_sensitive] = object.current_account.user.setting_default_sensitive
+    end
+
+    store
+  end
+
+  def accounts
+    store = {}
+    store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account
+    store[object.admin.id]           = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin
+    store
+  end
+
+  def media_attachments
+    { accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES }
+  end
+end
diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb
new file mode 100644
index 000000000..78376d253
--- /dev/null
+++ b/app/serializers/oembed_serializer.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class OEmbedSerializer < ActiveModel::Serializer
+  include RoutingHelper
+  include ActionView::Helpers::TagHelper
+
+  attributes :type, :version, :title, :author_name,
+             :author_url, :provider_name, :provider_url,
+             :cache_age, :html, :width, :height
+
+  def type
+    'rich'
+  end
+
+  def version
+    '1.0'
+  end
+
+  def author_name
+    object.account.display_name.presence || object.account.username
+  end
+
+  def author_url
+    account_url(object.account)
+  end
+
+  def provider_name
+    Rails.configuration.x.local_domain
+  end
+
+  def provider_url
+    root_url
+  end
+
+  def cache_age
+    86_400
+  end
+
+  def html
+    tag :iframe,
+        src: embed_account_stream_entry_url(object.account, object),
+        style: 'width: 100%; overflow: hidden',
+        frameborder: '0',
+        scrolling: 'no',
+        width: width,
+        height: height
+  end
+
+  def width
+    instance_options[:width]
+  end
+
+  def height
+    instance_options[:height]
+  end
+end
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
new file mode 100644
index 000000000..012a4fd18
--- /dev/null
+++ b/app/serializers/rest/account_serializer.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class REST::AccountSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :id, :username, :acct, :display_name, :locked, :created_at,
+             :note, :url, :avatar, :avatar_static, :header, :header_static,
+             :followers_count, :following_count, :statuses_count
+
+  def note
+    Formatter.instance.simplified_format(object)
+  end
+
+  def url
+    TagManager.instance.url_for(object)
+  end
+
+  def avatar
+    full_asset_url(object.avatar_original_url)
+  end
+
+  def avatar_static
+    full_asset_url(object.avatar_static_url)
+  end
+
+  def header
+    full_asset_url(object.header_original_url)
+  end
+
+  def header_static
+    full_asset_url(object.header_static_url)
+  end
+end
diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb
new file mode 100644
index 000000000..868a62f1e
--- /dev/null
+++ b/app/serializers/rest/application_serializer.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class REST::ApplicationSerializer < ActiveModel::Serializer
+  attributes :id, :name, :website, :redirect_uri,
+             :client_id, :client_secret
+
+  def client_id
+    object.uid
+  end
+
+  def client_secret
+    object.secret
+  end
+end
diff --git a/app/serializers/rest/context_serializer.rb b/app/serializers/rest/context_serializer.rb
new file mode 100644
index 000000000..44515c85d
--- /dev/null
+++ b/app/serializers/rest/context_serializer.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class REST::ContextSerializer < ActiveModel::Serializer
+  has_many :ancestors,   serializer: REST::StatusSerializer
+  has_many :descendants, serializer: REST::StatusSerializer
+end
diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb
new file mode 100644
index 000000000..870d8b71f
--- /dev/null
+++ b/app/serializers/rest/credential_account_serializer.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class REST::CredentialAccountSerializer < REST::AccountSerializer
+  attributes :source
+
+  def source
+    user = object.user
+    {
+      privacy: user.setting_default_privacy,
+      sensitive: user.setting_default_sensitive,
+      note: object.note,
+    }
+  end
+end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
new file mode 100644
index 000000000..8e32f9cb3
--- /dev/null
+++ b/app/serializers/rest/instance_serializer.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class REST::InstanceSerializer < ActiveModel::Serializer
+  attributes :uri, :title, :description, :email,
+             :version, :urls
+
+  def uri
+    Rails.configuration.x.local_domain
+  end
+
+  def title
+    Setting.site_title
+  end
+
+  def description
+    Setting.site_description
+  end
+
+  def email
+    Setting.site_contact_email
+  end
+
+  def version
+    Mastodon::Version.to_s
+  end
+
+  def urls
+    { streaming_api: Rails.configuration.x.streaming_api_base_url }
+  end
+end
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
new file mode 100644
index 000000000..9055b8db4
--- /dev/null
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class REST::MediaAttachmentSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :id, :type, :url, :preview_url,
+             :remote_url, :text_url, :meta
+
+  def url
+    full_asset_url(object.file.url(:original))
+  end
+
+  def preview_url
+    full_asset_url(object.file.url(:small))
+  end
+
+  def text_url
+    object.local? ? medium_url(object) : nil
+  end
+
+  def meta
+    object.file.meta
+  end
+end
diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb
new file mode 100644
index 000000000..f95d099a3
--- /dev/null
+++ b/app/serializers/rest/notification_serializer.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class REST::NotificationSerializer < ActiveModel::Serializer
+  attributes :id, :type, :created_at
+
+  belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
+  belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
+
+  def status_type?
+    [:favourite, :reblog, :mention].include?(object.type)
+  end
+end
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
new file mode 100644
index 000000000..9c460332c
--- /dev/null
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class REST::PreviewCardSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :url, :title, :description, :type,
+             :author_name, :author_url, :provider_name,
+             :provider_url, :html, :width, :height,
+             :image
+
+  def image
+    object.image? ? full_asset_url(object.image.url(:original)) : nil
+  end
+end
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
new file mode 100644
index 000000000..1d431aa1b
--- /dev/null
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class REST::RelationshipSerializer < ActiveModel::Serializer
+  attributes :id, :following, :followed_by, :blocking,
+             :muting, :requested, :domain_blocking
+
+  def following
+    instance_options[:relationships].following[object.id] || false
+  end
+
+  def followed_by
+    instance_options[:relationships].followed_by[object.id] || false
+  end
+
+  def blocking
+    instance_options[:relationships].blocking[object.id] || false
+  end
+
+  def muting
+    instance_options[:relationships].muting[object.id] || false
+  end
+
+  def requested
+    instance_options[:relationships].requested[object.id] || false
+  end
+
+  def domain_blocking
+    instance_options[:relationships].domain_blocking[object.id] || false
+  end
+end
diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb
new file mode 100644
index 000000000..0c6bd6556
--- /dev/null
+++ b/app/serializers/rest/report_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class REST::ReportSerializer < ActiveModel::Serializer
+  attributes :id, :action_taken
+end
diff --git a/app/serializers/rest/search_serializer.rb b/app/serializers/rest/search_serializer.rb
new file mode 100644
index 000000000..157f543ae
--- /dev/null
+++ b/app/serializers/rest/search_serializer.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class REST::SearchSerializer < ActiveModel::Serializer
+  attributes :hashtags
+
+  has_many :accounts, serializer: REST::AccountSerializer
+  has_many :statuses, serializer: REST::StatusSerializer
+
+  def hashtags
+    object.hashtags.map(&:name)
+  end
+end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
new file mode 100644
index 000000000..246b12a90
--- /dev/null
+++ b/app/serializers/rest/status_serializer.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+class REST::StatusSerializer < ActiveModel::Serializer
+  attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
+             :sensitive, :spoiler_text, :visibility, :language,
+             :uri, :content, :url, :reblogs_count, :favourites_count
+
+  attribute :favourited, if: :current_user?
+  attribute :reblogged, if: :current_user?
+  attribute :muted, if: :current_user?
+
+  belongs_to :reblog, serializer: REST::StatusSerializer
+  belongs_to :application
+  belongs_to :account, serializer: REST::AccountSerializer
+
+  has_many :media_attachments, serializer: REST::MediaAttachmentSerializer
+  has_many :mentions
+  has_many :tags
+
+  def current_user?
+    !current_user.nil?
+  end
+
+  def uri
+    TagManager.instance.uri_for(object)
+  end
+
+  def content
+    Formatter.instance.format(object)
+  end
+
+  def url
+    TagManager.instance.url_for(object)
+  end
+
+  def favourited
+    if instance_options && instance_options[:relationships]
+      instance_options[:relationships].favourites_map[object.id] || false
+    else
+      current_user.account.favourited?(object)
+    end
+  end
+
+  def reblogged
+    if instance_options && instance_options[:relationships]
+      instance_options[:relationships].reblogs_map[object.id] || false
+    else
+      current_user.account.reblogged?(object)
+    end
+  end
+
+  def muted
+    if instance_options && instance_options[:relationships]
+      instance_options[:relationships].mutes_map[object.conversation_id] || false
+    else
+      current_user.account.muting_conversation?(object.conversation)
+    end
+  end
+
+  class ApplicationSerializer < ActiveModel::Serializer
+    attributes :name, :website
+  end
+
+  class MentionSerializer < ActiveModel::Serializer
+    attributes :id, :username, :url, :acct
+
+    def id
+      object.account_id
+    end
+
+    def username
+      object.account_username
+    end
+
+    def url
+      TagManager.instance.url_for(object.account)
+    end
+
+    def acct
+      object.account_acct
+    end
+  end
+
+  class TagSerializer < ActiveModel::Serializer
+    include RoutingHelper
+
+    attributes :name, :url
+
+    def url
+      tag_url(object)
+    end
+  end
+end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 3b74696d5..47a47a735 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -54,7 +54,7 @@ class FanOutOnWriteService < BaseService
   end
 
   def render_anonymous_payload(status)
-    @payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show')
+    @payload = InlineRenderer.render(status, nil, :status)
     @payload = Oj.dump(event: :update, payload: @payload)
   end
 
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
index 8f42db0aa..d430b22e9 100644
--- a/app/services/fetch_atom_service.rb
+++ b/app/services/fetch_atom_service.rb
@@ -20,6 +20,10 @@ class FetchAtomService < BaseService
     process_html(fetch(url))
   rescue OpenSSL::SSL::SSLError => e
     Rails.logger.debug "SSL error: #{e}"
+    nil
+  rescue HTTP::ConnectionError => e
+    Rails.logger.debug "HTTP ConnectionError: #{e}"
+    nil
   end
 
   private
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index c2df7b2f0..6ef3abb66 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -18,6 +18,8 @@ class FetchLinkCardService < BaseService
     return if res.code != 200 || res.mime_type != 'text/html'
 
     attempt_opengraph(card, url) unless attempt_oembed(card, url)
+  rescue HTTP::ConnectionError, OpenSSL::SSL::SSLError
+    nil
   end
 
   private
@@ -82,7 +84,13 @@ class FetchLinkCardService < BaseService
 
     return if response.code != 200 || response.mime_type != 'text/html'
 
-    page = Nokogiri::HTML(response.to_s)
+    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))
 
     card.type             = :link
     card.title            = meta_property(page, 'og:title') || page.at_xpath('//title')&.content
diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb
index 8eed0d454..1efac365b 100644
--- a/app/services/fetch_remote_account_service.rb
+++ b/app/services/fetch_remote_account_service.rb
@@ -32,5 +32,8 @@ class FetchRemoteAccountService < BaseService
   rescue Nokogiri::XML::XPath::SyntaxError
     Rails.logger.debug 'Invalid XML or missing namespace'
     nil
+  rescue Goldfinger::NotFoundError, Goldfinger::Error
+    Rails.logger.debug 'Exceptions related to Goldfinger occurs'
+    nil
   end
 end
diff --git a/app/services/fetch_remote_resource_service.rb b/app/services/fetch_remote_resource_service.rb
index 5dfa3a0ff..2c1c1f05f 100644
--- a/app/services/fetch_remote_resource_service.rb
+++ b/app/services/fetch_remote_resource_service.rb
@@ -5,6 +5,9 @@ class FetchRemoteResourceService < BaseService
 
   def call(url)
     @url = url
+
+    return process_local_url if local_url?
+
     process_url unless fetched_atom_feed.nil?
   end
 
@@ -38,4 +41,29 @@ class FetchRemoteResourceService < BaseService
   def xml_data
     @_xml_data ||= Nokogiri::XML(body, nil, 'utf-8')
   end
+
+  def local_url?
+    TagManager.instance.local_url?(@url)
+  end
+
+  def process_local_url
+    recognized_params = Rails.application.routes.recognize_path(@url)
+
+    return unless recognized_params[:action] == 'show'
+
+    if recognized_params[:controller] == 'stream_entries'
+      status = StreamEntry.find_by(id: recognized_params[:id])&.status
+      check_local_status(status)
+    elsif recognized_params[:controller] == 'statuses'
+      status = Status.find_by(id: recognized_params[:id])
+      check_local_status(status)
+    elsif recognized_params[:controller] == 'accounts'
+      Account.find_local(recognized_params[:username])
+    end
+  end
+
+  def check_local_status(status)
+    return if status.nil?
+    status if status.public_visibility? || status.unlisted_visibility?
+  end
 end
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index f414813ad..4cfd33d90 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -33,6 +33,9 @@ class FetchRemoteStatusService < BaseService
   rescue Nokogiri::XML::XPath::SyntaxError
     Rails.logger.debug 'Invalid XML or missing namespace'
     nil
+  rescue Goldfinger::NotFoundError, Goldfinger::Error
+    Rails.logger.debug 'Exceptions related to Goldfinger occurs'
+    nil
   end
 
   def confirmed_domain?(domain, account)
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 422d5f97e..407d385ea 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -60,7 +60,7 @@ class NotifyService < BaseService
   def create_notification
     @notification.save!
     return unless @notification.browserable?
-    Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
+    Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
   end
 
   def send_email
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index 83765bb05..85635a008 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -13,21 +13,16 @@ class PrecomputeFeedService < BaseService
   attr_reader :account
 
   def populate_feed
-    redis.pipelined do
-      statuses.each do |status|
-        process_status(status)
-      end
+    pairs = statuses.reverse_each.lazy.reject(&method(:status_filtered?)).map(&method(:process_status)).to_a
 
+    redis.pipelined do
+      redis.zadd(account_home_key, pairs) if pairs.any?
       redis.del("account:#{@account.id}:regeneration")
     end
   end
 
   def process_status(status)
-    add_status_to_feed(status) unless status_filtered?(status)
-  end
-
-  def add_status_to_feed(status)
-    redis.zadd(account_home_key, status.id, status.reblog? ? status.reblog_of_id : status.id)
+    [status.id, status.reblog? ? status.reblog_of_id : status.id]
   end
 
   def status_filtered?(status)
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index fbdf92caa..c335d2159 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -20,8 +20,6 @@ class ProcessFeedService < BaseService
   end
 
   class ProcessEntry
-    include AuthorExtractor
-
     def call(xml, account)
       @account = account
       @xml     = xml
@@ -42,7 +40,7 @@ class ProcessFeedService < BaseService
     private
 
     def create_status
-      if redis.exists("delete_upon_arrival:#{id}")
+      if redis.exists("delete_upon_arrival:#{@account.id}:#{id}")
         Rails.logger.debug "Delete for status #{id} was queued, ignoring"
         return
       end
@@ -99,15 +97,13 @@ class ProcessFeedService < BaseService
 
     def delete_status
       Rails.logger.debug "Deleting remote status #{id}"
-      status = Status.find_by(uri: id)
+      status = Status.find_by(uri: id, account: @account)
 
       if status.nil?
-        redis.setex("delete_upon_arrival:#{id}", 6 * 3_600, id)
+        redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id)
       else
         RemoveStatusService.new.call(status)
       end
-
-      nil
     end
 
     def skip_unsupported_type?
@@ -128,18 +124,7 @@ class ProcessFeedService < BaseService
 
       return [status, false] unless status.nil?
 
-      # If status embeds an author, find that author
-      # If that author cannot be found, don't record the status (do not misattribute)
-      if account?(entry)
-        begin
-          account = author_from_xml(entry)
-          return [nil, false] if account.nil?
-        rescue Goldfinger::Error
-          return [nil, false]
-        end
-      else
-        account = @account
-      end
+      account = @account
 
       return [nil, false] if account.suspended?
 
diff --git a/app/views/about/_features.html.haml b/app/views/about/_features.html.haml
new file mode 100644
index 000000000..8fbc6b760
--- /dev/null
+++ b/app/views/about/_features.html.haml
@@ -0,0 +1,25 @@
+.features-list
+  .features-list__row
+    .text
+      %h6= t 'about.features.real_conversation_title'
+      = t 'about.features.real_conversation_body'
+    .visual
+      = fa_icon 'fw comments'
+  .features-list__row
+    .text
+      %h6= t 'about.features.not_a_product_title'
+      = t 'about.features.not_a_product_body'
+    .visual
+      = fa_icon 'fw users'
+  .features-list__row
+    .text
+      %h6= t 'about.features.within_reach_title'
+      = t 'about.features.within_reach_body'
+    .visual
+      = fa_icon 'fw mobile'
+  .features-list__row
+    .text
+      %h6= t 'about.features.humane_approach_title'
+      = t 'about.features.humane_approach_body'
+    .visual
+      = fa_icon 'fw leaf'
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 4f38c1ecc..eeeb0088f 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -1,10 +1,13 @@
 = simple_form_for(new_user, url: user_registration_path) do |f|
   = f.simple_fields_for :account do |account_fields|
-    = account_fields.input :username,
-      autofocus: true,
-      placeholder: t('simple_form.labels.defaults.username'),
-      required: true,
-      input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+    .input-with-append
+      = account_fields.input :username,
+        autofocus: true,
+        placeholder: t('simple_form.labels.defaults.username'),
+        required: true,
+        input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+      .append
+        = "@#{site_hostname}"
 
   = f.input :email,
     placeholder: t('simple_form.labels.defaults.email'),
@@ -22,9 +25,6 @@
     input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
 
   .actions
-    = f.button :button, t('about.get_started'), type: :submit
+    = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative'
 
-  .info
-    = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-    ·
-    = link_to t('about.about_this'), about_more_path
+  %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 2a7f8c752..f75f87c99 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -1,4 +1,5 @@
 - content_for :header_tags do
+  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
 
 - content_for :page_title do
@@ -9,79 +10,70 @@
   %meta{ property: 'og:url', content: about_url }/
   %meta{ property: 'og:type', content: 'website' }/
   %meta{ property: 'og:title', content: site_hostname }/
-  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon')) }/
+  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/
   %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/
   %meta{ property: 'og:image:width', content: '400' }/
   %meta{ property: 'og:image:height', content: '400' }/
   %meta{ property: 'twitter:card', content: 'summary' }/
 
-.wrapper
-  %h1
-    = image_tag asset_pack_path('logo.png')
-    = Setting.site_title
+.landing-page
+  .header-wrapper
+    .mascot-container
+      = image_tag asset_pack_path('elephant-fren.png'), class: 'mascot'
 
-  %p!= t('about.about_mastodon')
+    .header
+      .container.links
+        .brand
+          = link_to root_url do
+            = image_tag asset_pack_path('logo.svg')
+            Mastodon
 
-  .screenshot-with-signup
-    .mascot= image_tag asset_pack_path('fluffy-elephant-friend.png')
+        %ul.nav
+          %li
+            - if user_signed_in?
+              = link_to t('settings.back'), root_url, class: 'webapp-btn'
+            - else
+              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+          %li= link_to t('about.about_this'), about_more_path
+          %li= link_to t('about.other_instances'), 'https://joinmastodon.org/'
 
-    - if @instance_presenter.open_registrations
-      = render 'registration'
-    - else
-      .closed-registrations-message
-        - if @instance_presenter.closed_registrations_message.blank?
-          %p= t('about.closed_registrations')
+      .container.hero
+        .floats
+          = image_tag asset_pack_path('cloud2.png'), class: 'float-1'
+          = image_tag asset_pack_path('cloud3.png'), class: 'float-2'
+          = image_tag asset_pack_path('cloud4.png'), class: 'float-3'
+        .heading
+          %h1
+            = @instance_presenter.site_title
+            %small= t 'about.hosted_on', domain: site_hostname
+        - if @instance_presenter.open_registrations
+          = render 'registration'
         - else
-          != @instance_presenter.closed_registrations_message
-        .info
-          = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          ·
-          = link_to t('about.other_instances'), 'https://instances.mastodon.xyz/'
-          ·
-          = link_to t('about.about_this'), about_more_path
+          .closed-registrations-message
+            %div
+              - if @instance_presenter.closed_registrations_message.blank?
+                %p= t('about.closed_registrations')
+              - else
+                = @instance_presenter.closed_registrations_message.html_safe
+            = link_to t('about.find_another_instance'), 'https://joinmastodon.org', class: 'button button-alternative button--block'
 
-  %h3= t('about.features_headline')
+  .learn-more-cta
+    .container
+      %h3= t('about.description_headline', domain: site_hostname)
+      %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
 
-  .features-list
-    .features-list__column
-      %ul.fa-ul
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.chronology'
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.public'
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.characters'
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.gifv'
-    .features-list__column
-      %ul.fa-ul
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.privacy'
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.blocks'
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.ethics'
-        %li
-          = fa_icon('li check-square')
-          = t 'about.features.api'
+  .features
+    .container
+      - if Setting.timeline_preview
+        #mastodon-timeline{ data: { props: Oj.dump(default_props) } }
 
-  - unless @instance_presenter.site_description.blank?
-    %h3= t('about.description_headline', domain: site_hostname)
-    %p!= @instance_presenter.site_description
-
-  .actions
-    .info
-      = link_to t('about.terms'), terms_path
-      ·
-      = link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md'
-      ·
-      = link_to t('about.source_code'), 'https://github.com/chronister/mastodon'
-      ·
-      = link_to t('about.other_instances'), 'https://instances.mastodon.xyz/'
+      .about-mastodon
+        %h3= t 'about.what_is_mastodon'
+        %p= t 'about.about_mastodon_html'
+        %a.button.button-secondary{ href: 'https://joinmastodon.org' }= t 'about.learn_more'
+        = render 'features'
+  .footer-links
+    .container
+      %p
+        = link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
+        = " (#{@instance_presenter.version_number})"
diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml
deleted file mode 100644
index 7e0fb94c2..000000000
--- a/app/views/about/terms.en.html.haml
+++ /dev/null
@@ -1,76 +0,0 @@
-- content_for :page_title do
-  #{site_hostname} Terms of Service and Privacy Policy
-
-.wrapper
-  %h2 Privacy Policy
-
-  %h3#collect What information do we collect?
-
-  %p We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here.
-
-  %p When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address.
-
-  %p When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server.
-
-  %h3#use What do we use your information for?
-
-  %p Any of the information we collect from you may be used in one of the following ways:
-
-  %ul
-    %li To personalize your experience &mdash; your information helps us to better respond to your individual needs.
-    %li To improve our site &mdash; we continually strive to improve our site offerings based on the information and feedback we receive from you.
-    %li To improve customer service &mdash; your information helps us to more effectively respond to your customer service requests and support needs.
-    %li To send periodic emails &mdash; The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions.
-
-  %h3#protect How do we protect your information?
-
-  %p We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information.
-
-  %h3#data-retention What is your data retention policy?
-
-  %p We will make a good faith effort to:
-
-  %ul
-    %li Retain server logs containing the IP address of all requests to this server no more than 90 days.
-    %li Retain the IP addresses associated with registered users and their posts no more than 5 years.
-
-  %h3#cookies Do we use cookies?
-
-  %p Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.
-
-  %p We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business.
-
-  %h3#disclose Do we disclose any information to outside parties?
-
-  %p We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses.
-
-  %h3#third-party Third party links
-
-  %p Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites.
-
-  %h3#coppa Children's Online Privacy Protection Act Compliance
-
-  %p
-    Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA
-    = surround '(', '),' do
-      = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act'
-    do not use this site.
-
-  %h3#online Online Privacy Policy Only
-
-  %p This online privacy policy applies only to information collected through our site and not to information collected offline.
-
-  %h3#consent Your Consent
-
-  %p By using our site, you consent to our web site privacy policy.
-
-  %h3#changes Changes to our Privacy Policy
-
-  %p If we decide to change our privacy policy, we will post those changes on this page.
-
-  %p This document is CC-BY-SA. It was last updated May 31, 2013.
-
-  %p
-    Originally adapted from the
-    = succeed '.' do
-      = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse'
diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml
new file mode 100644
index 000000000..58064f0be
--- /dev/null
+++ b/app/views/about/terms.html.haml
@@ -0,0 +1,8 @@
+- content_for :page_title do
+  = t('terms.title', instance: site_hostname)
+
+.wrapper
+  - if @instance_presenter.site_terms.present?
+    = raw @instance_presenter.site_terms
+  - else
+    = t('terms.body_html')
diff --git a/app/views/about/terms.ja.html.haml b/app/views/about/terms.ja.html.haml
deleted file mode 100644
index 5c546b3e0..000000000
--- a/app/views/about/terms.ja.html.haml
+++ /dev/null
@@ -1,76 +0,0 @@
-- content_for :page_title do
-  #{site_hostname} 利用規約・プライバシーポリシー
-
-.wrapper
-  %h2 プライバシーポリシー
-
-  %h3#collect どのような情報を収集するのですか?
-
-  %p あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。
-
-  %p このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。
-
-  %p アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。
-
-  %h3#use 自分の情報を何に使うのですか?
-
-  %p このサイトで収集された情報は、次のいくつかの方法で使用されます:
-
-  %ul
-    %li パーソナライズ・エクスペリエンス &mdash; あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。
-    %li サイトの改善・最適化 &mdash; このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。
-    %li サービスの向上 &mdash; あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。
-    %li 定期メールの送信 &mdash; メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。
-
-  %h3#protect 自分の情報はどのように保護されるのですか?
-
-  %p このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。
-
-  %h3#data-retention データ保持のポリシーはどのようになっていますか?
-
-  %p このサービスはデータ保持に関して次のことを行うよう努めます。:
-
-  %ul
-    %li このサーバーへのすべての要求に対して、IPアドレスを含むサーバーログを90日以内に渡って保持します。
-    %li 登録されたユーザーとその投稿に関連付けされたIPアドレスを5年以内に渡って保持します。
-
-  %h3#cookies クッキーを使用していますか?
-
-  %p はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。
-
-  %p クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。
-
-  %h3#disclose このサイトは外部に何らかの情報を開示していますか?
-
-  %p 私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。
-
-  %h3#third-party サードパーティのリンク
-
-  %p 必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。
-
-  %h3#coppa 子供のオンライン・プライバシー保護法
-
-  %p
-    このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA
-    = surround '(', '),' do
-      = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act'
-    にもとづいてこのサイトを使用しないでください。
-
-  %h3#online オンライン限定のプライバシーポリシー
-
-  %p このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。
-
-  %h3#consent あなたの同意
-
-  %p このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。
-
-  %h3#changes プライバシーポリシーの変更
-
-  %p プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。
-
-  %p この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。
-
-  %p
-    オリジナルの出典
-    = succeed '.' do
-      = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse'
diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml
deleted file mode 100644
index 46f62950d..000000000
--- a/app/views/about/terms.no.html.haml
+++ /dev/null
@@ -1,76 +0,0 @@
-- content_for :page_title do
-  #{site_hostname} Personvern og villkår for bruk av nettstedet
-
-.wrapper
-  %h2 Personvernserklæring
-
-  %h3#collect Hvilke opplysninger samler vi?
-
-  %p Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her.
-
-  %p Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen.
-
-  %p Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår.
-
-  %h3#use Hva bruker vi opplysningene dine til?
-
-  %p Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter:
-
-  %ul
-    %li For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov.
-    %li For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg.
-    %li For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte.
-    %li For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål.
-
-  %h3#protect Hvordan sikrer vi opplysningene?
-
-  %p Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem.
-
-  %h3#data-retention Hva er retningslinjene deres for lagring av data?
-
-  %p Vi vil forsøke i god tro å:
-
-  %ul
-    %li Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager.
-    %li Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år.
-
-  %h3#cookies Bruker vi informasjonskapsler?
-
-  %p Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den.
-
-  %p Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt.
-
-  %h3#disclose Gir vi noen opplysninger videre til andre parter?
-
-  %p Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk.
-
-  %h3#third-party Tredjeparts lenker
-
-  %p Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne.
-
-  %h3#coppa Overensstemmelse med Children's Online Privacy Protection Act
-
-  %p
-    Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i  COPPA
-    = surround '(', '),' do
-      = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act'
-    ikke bruk dette nettstedet.
-
-  %h3#online Personvernerklæring bare for nettet
-
-  %p Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet.
-
-  %h3#consent Ditt samtykke
-
-  %p Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring.
-
-  %h3#changes Endringer i vår personvernerklæring
-
-  %p Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden.
-
-  %p Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017.
-
-  %p
-    Dokumentet er en adoptert og endret versjon fra
-    = succeed '.' do
-      = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse'
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 6d2849c32..07c8d1632 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -6,14 +6,30 @@
     %strong= t('admin.accounts.location.title')
     %ul
       %li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil
-      %li= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil
-      %li= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
+      %li
+        - if selected? local: '1', remote: nil
+          = filter_link_to t('admin.accounts.location.local'), {local: nil, remote: nil}, {local: '1', remote: nil}
+        - else
+          = filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil
+      %li
+        - if selected? remote: '1', local: nil
+          = filter_link_to t('admin.accounts.location.remote'), {remote: nil, local: nil}, {remote: '1', local: nil}
+        - else
+          = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
   .filter-subset
     %strong= t('admin.accounts.moderation.title')
     %ul
       %li= filter_link_to t('admin.accounts.moderation.all'), silenced: nil, suspended: nil
-      %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1'
-      %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
+      %li
+        - if selected? silenced: '1'
+          = filter_link_to t('admin.accounts.moderation.silenced'), {silenced: nil}, {silenced: '1'}
+        - else
+          = filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1'
+      %li
+        - if selected? suspended: '1'
+          = filter_link_to t('admin.accounts.moderation.suspended'), {suspended: nil}, {suspended: '1'}
+        - else
+          = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
   .filter-subset
     %strong= t('admin.accounts.order.title')
     %ul
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 965b71180..44486cb42 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -4,11 +4,11 @@
 .report-accounts
   .report-accounts__item
     %strong= t('admin.reports.reported_account')
-    = render 'authorize_follows/card', account: @report.target_account
+    = render 'authorize_follows/card', account: @report.target_account, admin: true
     = render 'admin/accounts/card', account: @report.target_account
   .report-accounts__item
     %strong= t('admin.reports.reported_by')
-    = render 'authorize_follows/card', account: @report.account
+    = render 'authorize_follows/card', account: @report.account, admin: true
     = render 'admin/accounts/card', account: @report.account
 
 %p
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index edb69e360..9f8a6640b 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -1,58 +1,32 @@
 - content_for :page_title do
   = t('admin.settings.title')
 
-= form_tag(admin_settings_path, method: :put, class: 'simple_form', style: 'max-width: 100%') do
-  %table.table
-    %thead
-      %tr
-        %th{ width: '40%' }
-          = t('admin.settings.setting')
-        %th
-    %tbody
-      %tr
-        %td
-          %strong= t('admin.settings.contact_information.label')
-        %td= text_field_tag :site_contact_username,
-          @settings['site_contact_username'].value,
-          place_holder: t('admin.settings.contact_information.username')
-      %tr
-        %td
-          %strong= t('admin.accounts.email')
-        %td= text_field_tag :site_contact_email,
-          @settings['site_contact_email'].value,
-          place_holder: t('admin.settings.contact_information.email')
-      %tr
-        %td
-          %strong= t('admin.settings.site_title')
-        %td= text_field_tag :site_title,
-          @settings['site_title'].value
-      %tr
-        %td
-          %strong= t('admin.settings.site_description.title')
-          %p= t('admin.settings.site_description.desc_html')
-        %td= text_area_tag :site_description,
-          @settings['site_description'].value,
-          rows: 8
-      %tr
-        %td
-          %strong= t('admin.settings.site_description_extended.title')
-          %p= t('admin.settings.site_description_extended.desc_html')
-        %td= text_area_tag :site_extended_description,
-          @settings['site_extended_description'].value,
-          rows: 8
-      %tr
-        %td
-          %strong= t('admin.settings.registrations.open.title')
-        %td
-          = select_tag :open_registrations,
-          options_for_select({ t('admin.settings.registrations.open.disabled') => false, t('admin.settings.registrations.open.enabled') => true }, @settings['open_registrations'].value)
-      %tr
-        %td
-          %strong= t('admin.settings.registrations.closed_message.title')
-          %p= t('admin.settings.registrations.closed_message.desc_html')
-        %td= text_area_tag :closed_registrations_message,
-          @settings['closed_registrations_message'].value,
-          rows: 8
-
-  .simple_form.actions
-    = button_tag t('generic.save_changes'), type: :submit, class: :btn
+= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f|
+  .fields-group
+    = f.input :site_title, placeholder: t('admin.settings.site_title')
+    = f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 8 }
+    = f.input :site_contact_username, placeholder: t('admin.settings.contact_information.username')
+    = f.input :site_contact_email, placeholder: t('admin.settings.contact_information.email')
+
+  %hr/
+
+  .fields-group
+    = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
+
+  .fields-group
+    = f.input :open_registrations, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.open.title'), hint: t('admin.settings.registrations.open.desc_html')
+
+  .fields-group
+    = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
+
+  .fields-group
+    = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
+
+  %hr/
+
+  .fields-group
+    = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
+    = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml
index 024788e13..1dec8e396 100644
--- a/app/views/admin/subscriptions/_subscription.html.haml
+++ b/app/views/admin/subscriptions/_subscription.html.haml
@@ -7,10 +7,12 @@
     - if subscription.confirmed?
       %i.fa.fa-check
   %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" }
-    = precede subscription.expired? ? '-' : '' do
-      = time_ago_in_words(subscription.expires_at)
+    %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) }
+      = precede subscription.expired? ? '-' : '' do
+        = time_ago_in_words(subscription.expires_at)
   %td
     - if subscription.last_successful_delivery_at?
-      = l subscription.last_successful_delivery_at
+      %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) }
+        = l subscription.last_successful_delivery_at
     - else
       %i.fa.fa-times
diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl
deleted file mode 100644
index 11dcec538..000000000
--- a/app/views/api/oembed/show.json.rabl
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-object @stream_entry
-
-node(:type) { 'rich' }
-node(:version) { '1.0' }
-node(:title, &:title)
-node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name }
-node(:author_url) { |entry| account_url(entry.account) }
-node(:provider_name) { site_hostname }
-node(:provider_url) { root_url }
-node(:cache_age) { 86_400 }
-node(:html) { |entry| "<iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"width: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" height=\"#{@height}\" scrolling=\"no\"></iframe>" }
-node(:width) { @width }
-node(:height) { @height }
diff --git a/app/views/api/v1/accounts/index.rabl b/app/views/api/v1/accounts/index.rabl
deleted file mode 100644
index 9f3b13a53..000000000
--- a/app/views/api/v1/accounts/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @accounts
-extends 'api/v1/accounts/show'
diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl
deleted file mode 100644
index 4f7763d9d..000000000
--- a/app/views/api/v1/accounts/relationship.rabl
+++ /dev/null
@@ -1,9 +0,0 @@
-object @account
-
-attribute :id
-node(:following)       { |account| @following[account.id]       || false }
-node(:followed_by)     { |account| @followed_by[account.id]     || false }
-node(:blocking)        { |account| @blocking[account.id]        || false }
-node(:muting)          { |account| @muting[account.id]          || false }
-node(:requested)       { |account| @requested[account.id]       || false }
-node(:domain_blocking) { |account| @domain_blocking[account.id] || false }
diff --git a/app/views/api/v1/accounts/relationships/index.rabl b/app/views/api/v1/accounts/relationships/index.rabl
deleted file mode 100644
index 022ea2ac4..000000000
--- a/app/views/api/v1/accounts/relationships/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @accounts
-extends 'api/v1/accounts/relationship'
diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl
deleted file mode 100644
index 8826aa22d..000000000
--- a/app/views/api/v1/accounts/show.rabl
+++ /dev/null
@@ -1,12 +0,0 @@
-object @account
-
-attributes :id, :username, :acct, :display_name, :locked, :created_at
-
-node(:note)            { |account| Formatter.instance.simplified_format(account) }
-node(:url)             { |account| TagManager.instance.url_for(account) }
-node(:avatar)          { |account| full_asset_url(account.avatar_original_url) }
-node(:avatar_static)   { |account| full_asset_url(account.avatar_static_url) }
-node(:header)          { |account| full_asset_url(account.header_original_url) }
-node(:header_static)   { |account| full_asset_url(account.header_static_url) }
-
-attributes :followers_count, :following_count, :statuses_count
diff --git a/app/views/api/v1/accounts/statuses/index.rabl b/app/views/api/v1/accounts/statuses/index.rabl
deleted file mode 100644
index 44d29d91b..000000000
--- a/app/views/api/v1/accounts/statuses/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @statuses
-extends 'api/v1/statuses/show'
diff --git a/app/views/api/v1/apps/create.rabl b/app/views/api/v1/apps/create.rabl
deleted file mode 100644
index 1ff6469a4..000000000
--- a/app/views/api/v1/apps/create.rabl
+++ /dev/null
@@ -1,4 +0,0 @@
-object @app
-attributes :id, :redirect_uri
-node(:client_id) { |app| app.uid }
-node(:client_secret) { |app| app.secret }
diff --git a/app/views/api/v1/apps/show.rabl b/app/views/api/v1/apps/show.rabl
deleted file mode 100644
index 6d9e607db..000000000
--- a/app/views/api/v1/apps/show.rabl
+++ /dev/null
@@ -1,3 +0,0 @@
-object @application
-
-attributes :name, :website
diff --git a/app/views/api/v1/blocks/index.rabl b/app/views/api/v1/blocks/index.rabl
deleted file mode 100644
index 9f3b13a53..000000000
--- a/app/views/api/v1/blocks/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @accounts
-extends 'api/v1/accounts/show'
diff --git a/app/views/api/v1/favourites/index.rabl b/app/views/api/v1/favourites/index.rabl
deleted file mode 100644
index 44d29d91b..000000000
--- a/app/views/api/v1/favourites/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @statuses
-extends 'api/v1/statuses/show'
diff --git a/app/views/api/v1/follow_requests/index.rabl b/app/views/api/v1/follow_requests/index.rabl
deleted file mode 100644
index 9f3b13a53..000000000
--- a/app/views/api/v1/follow_requests/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @accounts
-extends 'api/v1/accounts/show'
diff --git a/app/views/api/v1/follows/show.rabl b/app/views/api/v1/follows/show.rabl
deleted file mode 100644
index e07106164..000000000
--- a/app/views/api/v1/follows/show.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-object @account
-extends('api/v1/accounts/show')
diff --git a/app/views/api/v1/instances/show.rabl b/app/views/api/v1/instances/show.rabl
deleted file mode 100644
index 05fb65031..000000000
--- a/app/views/api/v1/instances/show.rabl
+++ /dev/null
@@ -1,10 +0,0 @@
-object false
-
-node(:uri)         { site_hostname }
-node(:title)       { Setting.site_title }
-node(:description) { Setting.site_description }
-node(:email)       { Setting.site_contact_email }
-node(:version)     { Mastodon::Version.to_s }
-node :urls do
-    { :streaming_api => Rails.configuration.x.streaming_api_base_url }
-end
diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl
deleted file mode 100644
index 53c13bbda..000000000
--- a/app/views/api/v1/media/create.rabl
+++ /dev/null
@@ -1,7 +0,0 @@
-object @media
-attribute :id, :type
-
-node(:url)         { |media| full_asset_url(media.file.url(:original)) }
-node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
-node(:text_url)    { |media| medium_url(media) }
-node(:meta)        { |media| media.file.meta }
diff --git a/app/views/api/v1/mutes/index.rabl b/app/views/api/v1/mutes/index.rabl
deleted file mode 100644
index 9f3b13a53..000000000
--- a/app/views/api/v1/mutes/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @accounts
-extends 'api/v1/accounts/show'
diff --git a/app/views/api/v1/notifications/index.rabl b/app/views/api/v1/notifications/index.rabl
deleted file mode 100644
index 6abc3da36..000000000
--- a/app/views/api/v1/notifications/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @notifications
-extends 'api/v1/notifications/show'
diff --git a/app/views/api/v1/notifications/show.rabl b/app/views/api/v1/notifications/show.rabl
deleted file mode 100644
index ca34f2d5d..000000000
--- a/app/views/api/v1/notifications/show.rabl
+++ /dev/null
@@ -1,11 +0,0 @@
-object @notification
-
-attributes :id, :type, :created_at
-
-child from_account: :account do
-  extends 'api/v1/accounts/show'
-end
-
-node(:status, if: lambda { |n| [:favourite, :reblog, :mention].include?(n.type) }) do |n|
-  partial 'api/v1/statuses/show', object: n.target_status
-end
diff --git a/app/views/api/v1/reports/index.rabl b/app/views/api/v1/reports/index.rabl
deleted file mode 100644
index 4f0794027..000000000
--- a/app/views/api/v1/reports/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @reports
-extends 'api/v1/reports/show'
diff --git a/app/views/api/v1/reports/show.rabl b/app/views/api/v1/reports/show.rabl
deleted file mode 100644
index 006db51e3..000000000
--- a/app/views/api/v1/reports/show.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-object @report
-attributes :id, :action_taken
diff --git a/app/views/api/v1/search/index.rabl b/app/views/api/v1/search/index.rabl
deleted file mode 100644
index 8d1640f2d..000000000
--- a/app/views/api/v1/search/index.rabl
+++ /dev/null
@@ -1,13 +0,0 @@
-object @search
-
-child :accounts, object_root: false do
-  extends 'api/v1/accounts/show'
-end
-
-node(:hashtags) do |search|
-  search.hashtags.map(&:name)
-end
-
-child :statuses, object_root: false do
-  extends 'api/v1/statuses/show'
-end
diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl
deleted file mode 100644
index 07ac31888..000000000
--- a/app/views/api/v1/statuses/_media.rabl
+++ /dev/null
@@ -1,6 +0,0 @@
-attributes :id, :remote_url, :type
-
-node(:url)         { |media| full_asset_url(media.file.url(:original)) }
-node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
-node(:text_url)    { |media| media.local? ? medium_url(media) : nil }
-node(:meta)        { |media| media.file.meta }
diff --git a/app/views/api/v1/statuses/_mention.rabl b/app/views/api/v1/statuses/_mention.rabl
deleted file mode 100644
index 8c95fc9bd..000000000
--- a/app/views/api/v1/statuses/_mention.rabl
+++ /dev/null
@@ -1,4 +0,0 @@
-node(:url)      { |mention| TagManager.instance.url_for(mention.account) }
-node(:acct)     { |mention| mention.account_acct }
-node(:id)       { |mention| mention.account_id }
-node(:username) { |mention| mention.account_username }
diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl
deleted file mode 100644
index fe3ec89ab..000000000
--- a/app/views/api/v1/statuses/_show.rabl
+++ /dev/null
@@ -1,29 +0,0 @@
-attributes :id, :created_at, :in_reply_to_id,
-           :in_reply_to_account_id, :sensitive,
-           :spoiler_text, :visibility, :language
-
-node(:uri)              { |status| TagManager.instance.uri_for(status) }
-node(:content)          { |status| Formatter.instance.format(status) }
-node(:url)              { |status| TagManager.instance.url_for(status) }
-node(:reblogs_count)    { |status| defined?(@reblogs_counts_map)    ? (@reblogs_counts_map[status.id]    || 0) : status.reblogs_count }
-node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count }
-
-child :application do
-  extends 'api/v1/apps/show'
-end
-
-child :account do
-  extends 'api/v1/accounts/show'
-end
-
-child :media_attachments, object_root: false do
-  extends 'api/v1/statuses/_media'
-end
-
-child :mentions, object_root: false do
-  extends 'api/v1/statuses/_mention'
-end
-
-child :tags, object_root: false do
-  extends 'api/v1/statuses/_tags'
-end
diff --git a/app/views/api/v1/statuses/_tags.rabl b/app/views/api/v1/statuses/_tags.rabl
deleted file mode 100644
index 25e7b0fac..000000000
--- a/app/views/api/v1/statuses/_tags.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-attribute :name
-node(:url) { |tag| tag_url(tag) }
diff --git a/app/views/api/v1/statuses/accounts.rabl b/app/views/api/v1/statuses/accounts.rabl
deleted file mode 100644
index 9f3b13a53..000000000
--- a/app/views/api/v1/statuses/accounts.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @accounts
-extends 'api/v1/accounts/show'
diff --git a/app/views/api/v1/statuses/card.rabl b/app/views/api/v1/statuses/card.rabl
deleted file mode 100644
index 5d8d7af3b..000000000
--- a/app/views/api/v1/statuses/card.rabl
+++ /dev/null
@@ -1,7 +0,0 @@
-object @card
-
-attributes :url, :title, :description, :type,
-           :author_name, :author_url, :provider_name,
-           :provider_url, :html, :width, :height
-
-node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil }
diff --git a/app/views/api/v1/statuses/context.rabl b/app/views/api/v1/statuses/context.rabl
deleted file mode 100644
index 0b62f26d5..000000000
--- a/app/views/api/v1/statuses/context.rabl
+++ /dev/null
@@ -1,9 +0,0 @@
-object @context
-
-node :ancestors do |context|
-  partial 'api/v1/statuses/index', object: context.ancestors
-end
-
-node :descendants do |context|
-  partial 'api/v1/statuses/index', object: context.descendants
-end
diff --git a/app/views/api/v1/statuses/index.rabl b/app/views/api/v1/statuses/index.rabl
deleted file mode 100644
index 0a0ed13c5..000000000
--- a/app/views/api/v1/statuses/index.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @statuses
-extends('api/v1/statuses/show')
diff --git a/app/views/api/v1/statuses/show.rabl b/app/views/api/v1/statuses/show.rabl
deleted file mode 100644
index 4b33fb2c3..000000000
--- a/app/views/api/v1/statuses/show.rabl
+++ /dev/null
@@ -1,15 +0,0 @@
-object @status
-
-extends 'api/v1/statuses/_show'
-
-node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id]         : current_account.favourited?(status) }
-node(:reblogged,  if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map)    ? @reblogs_map[status.id]            : current_account.reblogged?(status) }
-node(:muted,      if: proc { !current_account.nil? }) { |status| defined?(@mutes_map)      ? @mutes_map[status.conversation_id] : current_account.muting_conversation?(status.conversation) }
-
-child reblog: :reblog do
-  extends 'api/v1/statuses/_show'
-
-  node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) }
-  node(:reblogged,  if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map)    ? @reblogs_map[status.id]    : current_account.reblogged?(status) }
-  node(:muted,      if: proc { !current_account.nil? }) { false }
-end
diff --git a/app/views/api/v1/timelines/show.rabl b/app/views/api/v1/timelines/show.rabl
deleted file mode 100644
index 0a0ed13c5..000000000
--- a/app/views/api/v1/timelines/show.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @statuses
-extends('api/v1/statuses/show')
diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml
index 11c0d4e31..4521aad0a 100644
--- a/app/views/auth/registrations/_sessions.html.haml
+++ b/app/views/auth/registrations/_sessions.html.haml
@@ -11,9 +11,10 @@
     - @sessions.each do |session|
       %tr
         %td
-          %span{ title: session.user_agent }= fa_icon session_device_icon(session)
-          = ' '
-          = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
+          %span{ title: session.user_agent }<
+            = fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session)
+            = ' '
+            = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
         %td
           %samp= session.ip
         %td
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index 82d5483dd..af7ee2b28 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -5,7 +5,10 @@
   = render 'shared/error_messages', object: resource
 
   = f.simple_fields_for :account do |ff|
-    = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+    .input-with-append
+      = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+      .append
+        = "@#{site_hostname}"
 
   = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
   = f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
@@ -14,4 +17,5 @@
   .actions
     = f.button :button, t('auth.register'), type: :submit
 
+  %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml
index 13d9c7719..e81e292ba 100644
--- a/app/views/authorize_follows/_card.html.haml
+++ b/app/views/authorize_follows/_card.html.haml
@@ -4,7 +4,8 @@
       = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar'
 
     %span.display-name
-      = link_to TagManager.instance.url_for(account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
+      - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account)
+      = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
         %strong.emojify= display_name(account)
         %span @#{account.acct}
 
diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml
new file mode 100644
index 000000000..f0b495689
--- /dev/null
+++ b/app/views/authorize_follows/success.html.haml
@@ -0,0 +1,16 @@
+- content_for :page_title do
+  = t('authorize_follow.title', acct: @account.acct)
+
+.form-container
+  .follow-prompt
+    - if @account.locked?
+      %h2= t('authorize_follow.follow_request')
+    - else
+      %h2= t('authorize_follow.following')
+
+    = render 'card', account: @account
+
+  .post-follow-actions
+    %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block'
+    %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block'
+    %div= t('authorize_follow.post_follow.close')
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 33c978c89..71dcb54c6 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,5 +1,5 @@
 - content_for :header_tags do
-  %script#initial-state{ type: 'application/json' }!= json_escape(render(file: 'home/initial_state', formats: :json))
+  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 
   = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous'
 
diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
deleted file mode 100644
index e305f8e7a..000000000
--- a/app/views/home/initial_state.json.rabl
+++ /dev/null
@@ -1,37 +0,0 @@
-object false
-
-node(:meta) do
-  {
-    streaming_api_base_url: @streaming_api_base_url,
-    access_token: @token,
-    locale: I18n.locale,
-    domain: site_hostname,
-    me: current_account.id,
-    admin: @admin.try(:id),
-    boost_modal: current_account.user.setting_boost_modal,
-    delete_modal: current_account.user.setting_delete_modal,
-    auto_play_gif: current_account.user.setting_auto_play_gif,
-  }
-end
-
-node(:compose) do
-  {
-    me: current_account.id,
-    default_privacy: current_account.user.setting_default_privacy,
-  }
-end
-
-node(:accounts) do
-  store = {}
-  store[current_account.id] = partial('api/v1/accounts/show', object: current_account)
-  store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil?
-  store
-end
-
-node(:media_attachments) do
-  {
-    accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES
-  }
-end
-
-node(:settings) { @web_settings }
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index f991bc74f..ef97fb127 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -18,8 +18,16 @@
         = ' - '
       = title
 
-    = stylesheet_pack_tag 'application', media: 'all'
+    = stylesheet_pack_tag 'common', media: 'all'
     = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
+
+    = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+    = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+    = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+    = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+    = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+    = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
     = csrf_meta_tags
 
diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml
index e5429a8ed..b4f1bd0f3 100644
--- a/app/views/layouts/auth.html.haml
+++ b/app/views/layouts/auth.html.haml
@@ -6,7 +6,8 @@
     .logo-container
       %h1
         = link_to root_path do
-          = image_tag asset_pack_path('logo.png')
+          = image_tag asset_pack_path('logo.svg')
+          Mastodon
 
     .form-container
       = render 'flashes'
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 5680c1ff9..4826f32f7 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -2,7 +2,7 @@
 %html{ lang: I18n.locale }
   %head
     %meta{ charset: 'utf-8' }/
-    = stylesheet_pack_tag 'application', media: 'all'
+    = stylesheet_pack_tag 'common', media: 'all'
     = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
     = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 721ce6a21..56a261ab6 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -24,6 +24,8 @@
 
     = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 
+    = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label
+
   .fields-group
     = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
       = ff.input :follow, as: :boolean, wrapper: :with_label
@@ -44,6 +46,7 @@
 
   .fields-group
     = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
+    = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml
index 0bd6314d0..fb42d3f57 100644
--- a/app/views/stream_entries/_content_spoiler.html.haml
+++ b/app/views/stream_entries/_content_spoiler.html.haml
@@ -1,3 +1,7 @@
-.media-spoiler><
-  %span= t('stream_entries.sensitive_content')
-  %span= t('stream_entries.click_to_show')
+.media-spoiler-wrapper{ class: sensitive == false && 'media-spoiler-wrapper__visible' }><
+  .spoiler-button
+    .icon-button.overlayed
+      %i.fa.fa-fw.fa-eye
+  .media-spoiler
+    %span= t('stream_entries.sensitive_content')
+    %span= t('stream_entries.click_to_show')
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 589d647ce..157a7e7fb 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -18,13 +18,11 @@
       - unless status.media_attachments.empty?
         - if status.media_attachments.first.video?
           .video-player><
-            - if status.sensitive?
-              = render partial: 'stream_entries/content_spoiler'
+            = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? }
             %video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true }
         - else
           .detailed-status__attachments><
-            - if status.sensitive?
-              = render partial: 'stream_entries/content_spoiler'
+            = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? }
             .status__attachments__inner<
               - status.media_attachments.each do |media|
                 = render partial: 'stream_entries/media', locals: { media: media }
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index 88750180f..b44f9820f 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -23,8 +23,7 @@
 
       - unless status.media_attachments.empty?
         .status__attachments><
-          - if status.sensitive?
-            = render partial: 'stream_entries/content_spoiler'
+          = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? }
           - if status.media_attachments.first.video?
             .status__attachments__inner<
               .video-item<
diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb
index b0b3d0f51..fe3f0a010 100644
--- a/app/views/user_mailer/confirmation_instructions.fr.html.erb
+++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb
@@ -5,10 +5,10 @@
 <p>Pour confirmer votre inscription, merci de cliquer sur le lien suivant : <br>
 <%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>
 
-<p>Après votre première connexion, vous pourrez accéder à la documentation de l'outil.</p>
+<p>Après votre première connexion, vous pourrez accéder à la documentation de l’outil.</p>
 
 <p>Pensez également à jeter un œil à nos <%= link_to 'conditions d\'utilisation', terms_url %>.</p>
 
 <p>Amicalement,</p>
 
-<p>L'équipe <%= @instance %></p>
\ No newline at end of file
+<p>L’équipe <%= @instance %></p>
diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb
index cf8e39689..7730715f8 100644
--- a/app/views/user_mailer/confirmation_instructions.fr.text.erb
+++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb
@@ -5,10 +5,10 @@ Vous venez de vous créer un compte sur <%= @instance %> et nous vous en remerci
 Pour confirmer votre inscription, merci de cliquer sur le lien suivant :
 <%= confirmation_url(@resource, confirmation_token: @token) %>
 
-Après votre première connexion, vous pourrez accéder à la documentation de l'outil.
+Après votre première connexion, vous pourrez accéder à la documentation de l’outil.
 
-Pour rappel, nos conditions d'utilisation sont indiquées ici <%= terms_url %>
+Pour rappel, nos conditions d’utilisation sont indiquées ici <%= terms_url %>
 
 Amicalement,
 
-L'équipe <%= @instance %>
\ No newline at end of file
+L’équipe <%= @instance %>
diff --git a/app/views/user_mailer/password_change.pl.html.erb b/app/views/user_mailer/password_change.pl.html.erb
index 46423483a..a7cb15a05 100644
--- a/app/views/user_mailer/password_change.pl.html.erb
+++ b/app/views/user_mailer/password_change.pl.html.erb
@@ -1,3 +1,3 @@
 <p>Witaj, <%= @resource.email %>!</p>
 
-<p>Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.</p>
+<p>Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.</p>
diff --git a/app/views/user_mailer/password_change.pl.text.erb b/app/views/user_mailer/password_change.pl.text.erb
index 85d5e1175..bd2efee0f 100644
--- a/app/views/user_mailer/password_change.pl.text.erb
+++ b/app/views/user_mailer/password_change.pl.text.erb
@@ -1,3 +1,3 @@
 Witaj, <%= @resource.email %>!
 
-Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.
+Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.
diff --git a/app/views/user_mailer/reset_password_instructions.fr.html.erb b/app/views/user_mailer/reset_password_instructions.fr.html.erb
index 95789e387..db55c5884 100644
--- a/app/views/user_mailer/reset_password_instructions.fr.html.erb
+++ b/app/views/user_mailer/reset_password_instructions.fr.html.erb
@@ -1,8 +1,8 @@
 <p>Bonjour <%= @resource.email %>&nbsp;!</p>
 
-<p>Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.</p>
+<p>Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.</p>
 
 <p><%= link_to 'Modifier mon mot de passe', edit_password_url(@resource, reset_password_token: @token) %></p>
 
-<p>Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.</p>
-<p>Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.</p>
+<p>Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message.</p>
+<p>Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau.</p>
diff --git a/app/views/user_mailer/reset_password_instructions.fr.text.erb b/app/views/user_mailer/reset_password_instructions.fr.text.erb
index 73160cb4c..07fa3644a 100644
--- a/app/views/user_mailer/reset_password_instructions.fr.text.erb
+++ b/app/views/user_mailer/reset_password_instructions.fr.text.erb
@@ -1,8 +1,8 @@
 Bonjour <%= @resource.email %> !
 
-Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.
+Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.
 
 <%= edit_password_url(@resource, reset_password_token: @token) %>
 
-Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.
-Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.
+Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message.
+Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau.
diff --git a/app/views/user_mailer/reset_password_instructions.pl.html.erb b/app/views/user_mailer/reset_password_instructions.pl.html.erb
index f4d67c724..2a9913a1d 100644
--- a/app/views/user_mailer/reset_password_instructions.pl.html.erb
+++ b/app/views/user_mailer/reset_password_instructions.pl.html.erb
@@ -1,6 +1,7 @@
 <p>Witaj, <%= @resource.email %>!</p>
 
-<p>Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.</p>
+<p>Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w 
+poniższy link.</p>
 
 <p><%= link_to 'Zmień moje hasło', edit_password_url(@resource, reset_password_token: @token) %></p>
 
diff --git a/app/views/user_mailer/reset_password_instructions.pl.text.erb b/app/views/user_mailer/reset_password_instructions.pl.text.erb
index 78d1cab0b..2b34afc48 100644
--- a/app/views/user_mailer/reset_password_instructions.pl.text.erb
+++ b/app/views/user_mailer/reset_password_instructions.pl.text.erb
@@ -1,6 +1,7 @@
 Witaj, <%= @resource.email %>!
 
-Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.
+Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w 
+poniższy link.
 
 <%= edit_password_url(@resource, reset_password_token: @token) %>
 
diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb
index 5b0956b6b..6865e7136 100644
--- a/app/workers/pubsubhubbub/subscribe_worker.rb
+++ b/app/workers/pubsubhubbub/subscribe_worker.rb
@@ -3,7 +3,20 @@
 class Pubsubhubbub::SubscribeWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'push'
+  sidekiq_options queue: 'push', retry: 10, unique: :until_executed
+
+  sidekiq_retry_in do |count|
+    case count
+    when 0
+      30.minutes.seconds
+    when 1
+      2.hours.seconds
+    when 2
+      12.hours.seconds
+    else
+      24.hours.seconds * (count - 2)
+    end
+  end
 
   def perform(account_id)
     account = Account.find(account_id)
diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb
index fbcdcf634..697cbd6a6 100644
--- a/app/workers/push_update_worker.rb
+++ b/app/workers/push_update_worker.rb
@@ -6,7 +6,7 @@ class PushUpdateWorker
   def perform(account_id, status_id)
     account = Account.find(account_id)
     status  = Status.find(status_id)
-    message = InlineRenderer.render(status, account, 'api/v1/statuses/show')
+    message = InlineRenderer.render(status, account, :status)
 
     Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
   rescue ActiveRecord::RecordNotFound