From 7d8e3721aea71315b0ef8e66cdc2ede0fe6ffc2a Mon Sep 17 00:00:00 2001
From: "Akihiko Odaki (@fn_aki@pawoo.net)" <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Wed, 28 Jun 2017 21:50:23 +0900
Subject: Overwrite old statuses with reblogs in PrecomputeFeedService (#3984)

---
 spec/models/feed_spec.rb                              | 6 +++---
 spec/rails_helper.rb                                  | 6 ++++++
 spec/services/precompute_feed_service_spec.rb         | 8 ++++----
 spec/workers/scheduler/feed_cleanup_scheduler_spec.rb | 7 +++++--
 4 files changed, 18 insertions(+), 9 deletions(-)

(limited to 'spec')

diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb
index 15033e9eb..1cdb3a783 100644
--- a/spec/models/feed_spec.rb
+++ b/spec/models/feed_spec.rb
@@ -8,13 +8,13 @@ RSpec.describe Feed, type: :model do
       Fabricate(:status, account: account, id: 2)
       Fabricate(:status, account: account, id: 3)
       Fabricate(:status, account: account, id: 10)
-      redis = double(zrevrangebyscore: [['val2', 2.0], ['val1', 1.0], ['val3', 3.0], ['deleted', 4.0]], exists: false)
-      allow(Redis).to receive(:current).and_return(redis)
+      Redis.current.zadd(FeedManager.instance.key(:home, account.id),
+                        [[4, 'deleted'], [3, 'val3'], [2, 'val2'], [1, 'val1']])
 
       feed = Feed.new(:home, account)
       results = feed.get(3)
 
-      expect(results.map(&:id)).to eq [2, 1, 3]
+      expect(results.map(&:id)).to eq [3, 2]
       expect(results.first.attributes.keys).to eq %w(id updated_at)
     end
   end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index cfc9eec9e..9a4c8fd3c 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -13,6 +13,7 @@ Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
 
 ActiveRecord::Migration.maintain_test_schema!
 WebMock.disable_net_connect!
+Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current)
 Sidekiq::Testing.inline!
 Sidekiq::Logging.logger = nil
 
@@ -43,6 +44,11 @@ RSpec.configure do |config|
     https = ENV['LOCAL_HTTPS'] == 'true'
     Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
   end
+
+  config.after :each do
+    keys = Redis.current.keys
+    Redis.current.del(keys) if keys.any?
+  end
 end
 
 RSpec::Sidekiq.configure do |config|
diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb
index 9f56b0256..72235a966 100644
--- a/spec/services/precompute_feed_service_spec.rb
+++ b/spec/services/precompute_feed_service_spec.rb
@@ -11,12 +11,12 @@ RSpec.describe PrecomputeFeedService do
       account = Fabricate(:account)
       followed_account = Fabricate(:account)
       Fabricate(:follow, account: account, target_account: followed_account)
-      status = Fabricate(:status, account: followed_account)
-
-      expected_redis_args = FeedManager.instance.key(:home, account.id), status.id, status.id
-      expect_any_instance_of(Redis).to receive(:zadd).with(*expected_redis_args)
+      reblog = Fabricate(:status, account: followed_account)
+      status = Fabricate(:status, account: account, reblog: reblog)
 
       subject.call(account)
+
+      expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id
     end
   end
 end
diff --git a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb
index 4c709a2c9..b8487b03f 100644
--- a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb
+++ b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb
@@ -7,10 +7,13 @@ describe Scheduler::FeedCleanupScheduler do
   let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) }
 
   it 'clears feeds of inactives' do
-    expect_any_instance_of(Redis).to receive(:del).with(feed_key_for(inactive_user))
-    expect_any_instance_of(Redis).not_to receive(:del).with(feed_key_for(active_user))
+    Redis.current.zadd(feed_key_for(inactive_user), 1, 1)
+    Redis.current.zadd(feed_key_for(active_user), 1, 1)
 
     subject.perform
+
+    expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0
+    expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1
   end
 
   def feed_key_for(user)
-- 
cgit 


From 60b2b56d380c7cd3dc0ba54f4650cfdba568e38e Mon Sep 17 00:00:00 2001
From: "Akihiko Odaki (@fn_aki@pawoo.net)" <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Thu, 29 Jun 2017 08:17:26 +0900
Subject: Reduce number of commands in FeedManager#trim (#3989)

---
 app/lib/feed_manager.rb       |  4 +---
 spec/lib/feed_manager_spec.rb | 13 +++++++++++++
 2 files changed, 14 insertions(+), 3 deletions(-)

(limited to 'spec')

diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 90a1441f2..c507f2636 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/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index bf474c354..4bdc96866 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -131,4 +131,17 @@ RSpec.describe FeedManager do
       end
     end
   end
+
+  describe '#push' do
+    it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do
+      account = Fabricate(:account)
+      status = Fabricate(:status)
+      members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] }
+      Redis.current.zadd("feed:type:#{account.id}", members)
+
+      FeedManager.instance.push('type', account, status)
+
+      expect(Redis.current.zcard("feed:type:#{account.id}")).to eq FeedManager::MAX_ITEMS
+    end
+  end
 end
-- 
cgit 


From 0a53ca444a4bd4b28eddf2064133c38a65d41f2c Mon Sep 17 00:00:00 2001
From: "Akihiko Odaki (@fn_aki@pawoo.net)" <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Thu, 29 Jun 2017 08:43:10 +0900
Subject: Cover Admin::AccountsController more (#3327)

---
 spec/controllers/admin/accounts_controller_spec.rb | 55 +++++++++++++++++++++-
 1 file changed, 54 insertions(+), 1 deletion(-)

(limited to 'spec')

diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 305260475..8be27d866 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -3,11 +3,64 @@ require 'rails_helper'
 RSpec.describe Admin::AccountsController, type: :controller do
   render_views
 
+  let(:user) { Fabricate(:user, admin: true) }
+
   before do
-    sign_in Fabricate(:user, admin: true), scope: :user
+    sign_in user, scope: :user
   end
 
   describe 'GET #index' do
+    around do |example|
+      default_per_page = Account.default_per_page
+      Account.paginates_per 1
+      example.run
+      Account.paginates_per default_per_page
+    end
+
+    it 'filters with parameters' do
+      new = AccountFilter.method(:new)
+
+      expect(AccountFilter).to receive(:new) do |params|
+        h = params.to_h
+
+        expect(h[:local]).to eq '1'
+        expect(h[:remote]).to eq '1'
+        expect(h[:by_domain]).to eq 'domain'
+        expect(h[:silenced]).to eq '1'
+        expect(h[:recent]).to eq '1'
+        expect(h[:suspended]).to eq '1'
+        expect(h[:username]).to eq 'username'
+        expect(h[:display_name]).to eq 'display name'
+        expect(h[:email]).to eq 'local-part@domain'
+        expect(h[:ip]).to eq '0.0.0.42'
+
+        new.call({})
+      end
+
+      get :index, params: {
+        local: '1',
+        remote: '1',
+        by_domain: 'domain',
+        silenced: '1',
+        recent: '1',
+        suspended: '1',
+        username: 'username',
+        display_name: 'display name',
+        email: 'local-part@domain',
+        ip: '0.0.0.42'
+      }
+    end
+
+    it 'paginates accounts' do
+      Fabricate(:account)
+
+      get :index, params: { page: 2 }
+
+      accounts = assigns(:accounts)
+      expect(accounts.count).to eq 1
+      expect(accounts.klass).to be Account
+    end
+
     it 'returns http success' do
       get :index
       expect(response).to have_http_status(:success)
-- 
cgit 


From ead14f5bf0ae43524f055320f373a6e2ce947476 Mon Sep 17 00:00:00 2001
From: Yamagishi Kazutoshi <ykzts@desire.sh>
Date: Thu, 29 Jun 2017 20:03:03 +0900
Subject: Upgrade jsdom to version 11.0.0 (#3994)

---
 package.json             |  2 +-
 spec/javascript/setup.js | 18 ++++++------------
 yarn.lock                | 20 +++++++++++++-------
 3 files changed, 20 insertions(+), 20 deletions(-)

(limited to 'spec')

diff --git a/package.json b/package.json
index 60f9af1e9..feb59dc90 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
     "eslint": "^3.19.0",
     "eslint-plugin-jsx-a11y": "^4.0.0",
     "eslint-plugin-react": "^6.10.3",
-    "jsdom": "^10.1.0",
+    "jsdom": "^11.0.0",
     "mocha": "^3.4.1",
     "react-intl-translations-manager": "^5.0.0",
     "react-test-renderer": "^15.6.1",
diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js
index 7d4b2866e..c9c8aed07 100644
--- a/spec/javascript/setup.js
+++ b/spec/javascript/setup.js
@@ -1,19 +1,13 @@
-import { jsdom } from 'jsdom/lib/old-api';
+import { JSDOM } from 'jsdom';
 import chai from 'chai';
 import chaiEnzyme from 'chai-enzyme';
 chai.use(chaiEnzyme());
 
-var exposedProperties = ['window', 'navigator', 'document'];
-
-global.document = jsdom('');
-global.window = document.defaultView;
-Object.keys(document.defaultView).forEach((property) => {
+const { window } = new JSDOM('', {
+  userAgent: 'node.js',
+});
+Object.keys(window).forEach(property => {
   if (typeof global[property] === 'undefined') {
-    exposedProperties.push(property);
-    global[property] = document.defaultView[property];
+    global[property] = window[property];
   }
 });
-
-global.navigator = {
-  userAgent: 'node.js',
-};
diff --git a/yarn.lock b/yarn.lock
index b8af49d62..4b5e3ae17 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -121,6 +121,10 @@
     react-split-pane "^0.1.63"
     redux "^3.6.0"
 
+"@types/node@^6.0.46":
+  version "6.0.78"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.78.tgz#5d4a3f579c1524e01ee21bf474e6fba09198f470"
+
 abab@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d"
@@ -3911,9 +3915,9 @@ jsbn@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
 
-jsdom@^10.1.0:
-  version "10.1.0"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-10.1.0.tgz#7765e00fd5c3567f34985a1c86ff466a61dacc6a"
+jsdom@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.0.0.tgz#1ee507cb2c0b16c875002476b1a8557d951353e5"
   dependencies:
     abab "^1.0.3"
     acorn "^4.0.4"
@@ -3925,7 +3929,7 @@ jsdom@^10.1.0:
     escodegen "^1.6.1"
     html-encoding-sniffer "^1.0.1"
     nwmatcher ">= 1.3.9 < 2.0.0"
-    parse5 "^1.5.1"
+    parse5 "^3.0.2"
     pn "^1.0.0"
     request "^2.79.0"
     request-promise-native "^1.0.3"
@@ -4919,9 +4923,11 @@ parse-json@^2.2.0:
   dependencies:
     error-ex "^1.2.0"
 
-parse5@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
+parse5@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510"
+  dependencies:
+    "@types/node" "^6.0.46"
 
 parseurl@~1.3.1:
   version "1.3.1"
-- 
cgit 


From 7362469d8956d5f972283aadd4157631aa66b085 Mon Sep 17 00:00:00 2001
From: "Akihiko Odaki (@fn_aki@pawoo.net)" <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Fri, 30 Jun 2017 20:39:42 +0900
Subject: Do not raise an error if PrecomputeFeed could not find any status
 (#4015)

---
 app/services/precompute_feed_service.rb       | 2 +-
 spec/services/precompute_feed_service_spec.rb | 5 +++++
 2 files changed, 6 insertions(+), 1 deletion(-)

(limited to 'spec')

diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index e2f6ff0cb..a32ba1dae 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -16,7 +16,7 @@ class PrecomputeFeedService < BaseService
     pairs = statuses.reverse_each.map(&method(:process_status))
 
     redis.pipelined do
-      redis.zadd(account_home_key, pairs)
+      redis.zadd(account_home_key, pairs) if pairs.any?
       redis.del("account:#{@account.id}:regeneration")
     end
   end
diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb
index 72235a966..e2294469c 100644
--- a/spec/services/precompute_feed_service_spec.rb
+++ b/spec/services/precompute_feed_service_spec.rb
@@ -18,5 +18,10 @@ RSpec.describe PrecomputeFeedService do
 
       expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id
     end
+
+    it 'does not raise an error even if it could not find any status' do
+      account = Fabricate(:account)
+      subject.call(account)
+    end
   end
 end
-- 
cgit 


From 6dd5eac7fc81f2283525f954db812d937153bcfc Mon Sep 17 00:00:00 2001
From: Matt Jankowski <mjankowski@thoughtbot.com>
Date: Fri, 30 Jun 2017 07:43:34 -0400
Subject: Add controller spec for manifests controller (#4003)

---
 spec/controllers/manifests_controller_spec.rb | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 spec/controllers/manifests_controller_spec.rb

(limited to 'spec')

diff --git a/spec/controllers/manifests_controller_spec.rb b/spec/controllers/manifests_controller_spec.rb
new file mode 100644
index 000000000..6f188fa35
--- /dev/null
+++ b/spec/controllers/manifests_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+describe ManifestsController do
+  render_views
+
+  describe 'GET #show' do
+    before do
+      get :show, format: :json
+    end
+
+    it 'assigns @instance_presenter' do
+      expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
-- 
cgit 


From a978b88997169782ac35f416bf88d6afd60edd1e Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Fri, 30 Jun 2017 08:29:22 -0700
Subject: Faster emojify() algorithm, avoid regex replace (#4019)

* Faster emojify() algorithm, avoid regex replace

* add semicolon
---
 app/javascript/mastodon/emoji.js           | 43 ++++++++++++++++++++------
 spec/javascript/components/emojify.test.js | 49 ++++++++++++++++++++++++++++++
 2 files changed, 83 insertions(+), 9 deletions(-)
 create mode 100644 spec/javascript/components/emojify.test.js

(limited to 'spec')

diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index 01d01fb72..d0df71ea3 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -19,16 +19,41 @@ const unicodeToImage = str => {
   });
 };
 
-const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => {
-  if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) {
-    return shortname;
+const shortnameToImage = str => {
+  // This walks through the string from end to start, ignoring any tags (<p>, <br>, etc.)
+  // and replacing valid shortnames like :smile: and :wink: that _aren't_ within
+  // tags with an <img> version.
+  // The goal is to be the same as an emojione.regShortNames replacement, but faster.
+  // The reason we go backwards is because then we can replace substrings as we go.
+  let i = str.length;
+  let insideTag = false;
+  let insideShortname = false;
+  let shortnameEndIndex = -1;
+  while (i--) {
+    const char = str.charAt(i);
+    if (insideShortname && char === ':') {
+      const shortname = str.substring(i, shortnameEndIndex + 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, i) + replacement + str.substring(shortnameEndIndex + 1);
+      } 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;
+      shortnameEndIndex = i;
+    }
   }
-
-  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" />`;
-});
+  return str;
+};
 
 export default function emojify(text) {
   return toImage(text);
diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js
new file mode 100644
index 000000000..7a496623e
--- /dev/null
+++ b/spec/javascript/components/emojify.test.js
@@ -0,0 +1,49 @@
+import { expect } from 'chai';
+import emojify from '../../../app/javascript/mastodon/emoji';
+
+describe('emojify', () => {
+  it('does a basic emojify', () => {
+    expect(emojify(':smile:')).to.equal(
+      '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />');
+  });
+
+  it('does a double emojify', () => {
+    expect(emojify(':smile: and :wink:')).to.equal(
+      '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /> and <img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
+  });
+
+  it('works with random colons', () => {
+    expect(emojify(':smile: : :wink:')).to.equal(
+      '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /> : <img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
+    expect(emojify(':smile::::wink:')).to.equal(
+      '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />::<img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
+    expect(emojify(':smile:::::wink:')).to.equal(
+      '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />:::<img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
+  });
+
+  it('works with tags', () => {
+    expect(emojify('<p>:smile:</p>')).to.equal(
+      '<p><img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /></p>');
+    expect(emojify('<p>:smile:</p> and <p>:wink:</p>')).to.equal(
+      '<p><img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /></p> and <p><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" /></p>');
+  });
+
+  it('ignores unknown shortcodes', () => {
+    expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:');
+  });
+
+  it('ignores shortcodes inside of tags', () => {
+    expect(emojify('<p data-foo=":smile:"></p>')).to.equal('<p data-foo=":smile:"></p>');
+  });
+
+  it('works with unclosed tags', () => {
+    expect(emojify('hello>')).to.equal('hello>');
+    expect(emojify('<hello')).to.equal('<hello');
+  });
+
+  it('works with unclosed shortcodes', () => {
+    expect(emojify('smile:')).to.equal('smile:');
+    expect(emojify(':smile')).to.equal(':smile');
+  });
+
+});
-- 
cgit 


From e28258010182b56f27cfbd3f9f9a58fd9cd8870d Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Mon, 3 Jul 2017 02:02:36 -0700
Subject: Faster emojify() by avoiding str.replace() entirely (#4049)

---
 app/javascript/mastodon/emoji.js           | 69 ++++++++++++++----------------
 package.json                               |  1 +
 spec/javascript/components/emojify.test.js | 34 +++++++++++++++
 yarn.lock                                  |  4 ++
 4 files changed, 71 insertions(+), 37 deletions(-)

(limited to 'spec')

diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index d0df71ea3..7043d5f3a 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -1,60 +1,55 @@
 import emojione from 'emojione';
+import Trie from 'substring-trie';
 
-const toImage = str => shortnameToImage(unicodeToImage(str));
+const mappedUnicode = emojione.mapUnicodeToShort();
+const trie = new Trie(Object.keys(emojione.jsEscapeMap));
 
-const unicodeToImage = str => {
-  const mappedUnicode = emojione.mapUnicodeToShort();
-
-  return str.replace(emojione.regUnicode, unicodeChar => {
-    if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) {
-      return unicodeChar;
-    }
-
-    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 => {
-  // This walks through the string from end to start, ignoring any tags (<p>, <br>, etc.)
-  // and replacing valid shortnames like :smile: and :wink: that _aren't_ within
-  // tags with an <img> version.
-  // The goal is to be the same as an emojione.regShortNames replacement, but faster.
-  // The reason we go backwards is because then we can replace substrings as we go.
-  let i = str.length;
+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 shortnameEndIndex = -1;
-  while (i--) {
+  let shortnameStartIndex = -1;
+  let match;
+  while (++i < str.length) {
     const char = str.charAt(i);
     if (insideShortname && char === ':') {
-      const shortname = str.substring(i, shortnameEndIndex + 1);
+      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, i) + replacement + str.substring(shortnameEndIndex + 1);
+        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
+        i--; // stray colon, try again
       }
       insideShortname = false;
-    } else if (insideTag && char === '<') {
+    } else if (insideTag && char === '>') {
       insideTag = false;
-    } else if (char === '>') {
+    } else if (char === '<') {
       insideTag = true;
       insideShortname = false;
     } else if (!insideTag && char === ':') {
       insideShortname = true;
-      shortnameEndIndex = i;
+      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
+      }
     }
   }
   return str;
-};
+}
 
-export default function emojify(text) {
-  return toImage(text);
-};
+export default emojify;
diff --git a/package.json b/package.json
index 7fa80a0c2..d5c05dae3 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,7 @@
     "sass-loader": "^6.0.6",
     "stringz": "^0.2.2",
     "style-loader": "^0.18.2",
+    "substring-trie": "^1.0.0",
     "throng": "^4.0.0",
     "tiny-queue": "^0.2.1",
     "uuid": "^3.1.0",
diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js
index 7a496623e..3e8b25af9 100644
--- a/spec/javascript/components/emojify.test.js
+++ b/spec/javascript/components/emojify.test.js
@@ -46,4 +46,38 @@ describe('emojify', () => {
     expect(emojify(':smile')).to.equal(':smile');
   });
 
+  it('does two emoji next to each other', () => {
+    expect(emojify(':smile::wink:')).to.equal(
+      '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
+  });
+
+  it('does unicode', () => {
+    expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal(
+      '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":family_wwbb:" src="/emoji/1f469-1f469-1f466-1f466.svg" />');
+    expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal(
+      '<img draggable="false" class="emojione" alt="👨👩👧👧" title=":family_mwgg:" src="/emoji/1f468-1f469-1f467-1f467.svg" />');
+    expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" title=":family_wwb:" src="/emoji/1f469-1f469-1f466.svg" />');
+    expect(emojify('\u2757')).to.equal(
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+  });
+
+  it('does multiple unicode', () => {
+    expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal(
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />');
+    expect(emojify('\u2757#\uFE0F\u20E3')).to.equal(
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />');
+    expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal(
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+    expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal(
+      'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> bar');
+  });
+
+  it('does mixed unicode and shortnames', () => {
+    expect(emojify(':smile:#\uFE0F\u20E3:wink:\u2757')).to.equal('<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" /><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+  });
+
+  it('ignores unicode inside of tags', () => {
+    expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).to.equal('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
+  });
+
 });
diff --git a/yarn.lock b/yarn.lock
index adabca08d..609f256c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6819,6 +6819,10 @@ style-loader@^0.18.2:
     loader-utils "^1.0.2"
     schema-utils "^0.3.0"
 
+substring-trie@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.0.tgz#5a7ecb83aefcca7b3720f7897cf69e97023be143"
+
 sugarss@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.0.tgz#65e51b3958432fb70d5451a68bb33e32d0cf1ef7"
-- 
cgit 


From f85dbe83c8e982f9685fbc802031b74c7c319bc7 Mon Sep 17 00:00:00 2001
From: "Akihiko Odaki (@fn_aki@pawoo.net)" <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Mon, 3 Jul 2017 20:17:27 +0900
Subject: Remove sort in Feed (#4050)

In from_redis method, statuses retrieved from the database was mapped to
the IDs retrieved from Redis. It was equivalent to order from high to low
because those IDs are sorted in the same order.
Statuses are ordered with the ID by default, so we do not have to reorder.
Sorting statuses in the database is even faster since the IDs are indexed
with B-tree.
---
 app/models/feed.rb       | 3 +--
 spec/models/feed_spec.rb | 2 +-
 2 files changed, 2 insertions(+), 3 deletions(-)

(limited to 'spec')

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/spec/models/feed_spec.rb b/spec/models/feed_spec.rb
index 1cdb3a783..1c377c17f 100644
--- a/spec/models/feed_spec.rb
+++ b/spec/models/feed_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
 
 RSpec.describe Feed, type: :model do
   describe '#get' do
-    it 'gets statuses with ids in the range, maintining the order from Redis' do
+    it 'gets statuses with ids in the range' do
       account = Fabricate(:account)
       Fabricate(:status, account: account, id: 1)
       Fabricate(:status, account: account, id: 2)
-- 
cgit 


From 5e6acf960183aea9440ce0d9e28c86f043e88c54 Mon Sep 17 00:00:00 2001
From: abcang <abcang1015@gmail.com>
Date: Wed, 5 Jul 2017 21:54:21 +0900
Subject: Fix Nokogiri::HTML at FetchLinkCardService (#4072)

---
 app/services/fetch_link_card_service.rb       |  4 +++-
 spec/fixtures/requests/sjis.txt               | 20 ++++++++++++++++++++
 spec/services/fetch_link_card_service_spec.rb | 10 ++++++++++
 3 files changed, 33 insertions(+), 1 deletion(-)
 create mode 100644 spec/fixtures/requests/sjis.txt

(limited to 'spec')

diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 4ce221267..8ddaa2bf4 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -1,4 +1,5 @@
 # frozen_string_literal: true
+require 'nkf'
 
 class FetchLinkCardService < BaseService
   include HttpHelper
@@ -84,7 +85,8 @@ class FetchLinkCardService < BaseService
 
     return if response.code != 200 || response.mime_type != 'text/html'
 
-    page = Nokogiri::HTML(response.to_s)
+    html = response.to_s
+    page = Nokogiri::HTML(html, nil, NKF.guess(html).to_s)
 
     card.type             = :link
     card.title            = meta_property(page, 'og:title') || page.at_xpath('//title')&.content
diff --git a/spec/fixtures/requests/sjis.txt b/spec/fixtures/requests/sjis.txt
new file mode 100644
index 000000000..9041aa25d
--- /dev/null
+++ b/spec/fixtures/requests/sjis.txt
@@ -0,0 +1,20 @@
+HTTP/1.1 200 OK
+Server: nginx/1.11.10
+Date: Tue, 04 Jul 2017 16:43:39 GMT
+Content-Type: text/html
+Content-Length: 273
+Connection: keep-alive
+Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT
+Accept-Ranges: bytes
+
+<HTML>
+<HEAD>
+  <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac">
+  <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=x-sjis">
+  <TITLE>JSIS�̃y�[�W</TITLE>
+</HEAD>
+<BODY>
+<P><CENTER><B><FONT SIZE="+2">SJIS�̃y�[�W</FONT></B><BR>
+<HR><BR>
+</BODY>
+</HTML>
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 9df41cf55..7d7f8e748 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe FetchLinkCardService do
   before do
     stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
     stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt'))
+    stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
+    stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt'))
     stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404)
 
     subject.call(status)
@@ -19,6 +21,14 @@ RSpec.describe FetchLinkCardService do
         expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once
       end
     end
+
+    context do
+      let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') }
+
+      it 'works with SJIS' do
+        expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once
+      end
+    end
   end
 
   context 'in a remote status' do
-- 
cgit 


From 18d3fa953b5af8ab17cc93c33cb95cec37127712 Mon Sep 17 00:00:00 2001
From: Damien Erambert <damien.erambert@me.com>
Date: Thu, 6 Jul 2017 13:39:56 -0700
Subject: Add a setting allowing the use of system's default font in Web UI
 (#4033)

* add a system_font_ui setting on the server

* Plug the system_font_ui on the front-end

* add EN/FR locales for the new setting

* put Roboto after all other fonts

* remove trailing whitespace so CodeClimate is happy

* fix user_spec.rb

* correctly write user_spect this time

* slightly better way of adding the classes

* add comments to the system-font stack for clarification

* use .system-font for the class instead

* don't use multiple lines for comments

* remove trailing whitespace

* use the classnames module for consistency

* use `mastodon-font-sans-serif` instead of Roboto directly
---
 app/controllers/settings/preferences_controller.rb |  1 +
 app/javascript/mastodon/features/ui/index.js       | 14 ++++++++++++--
 app/javascript/styles/basics.scss                  | 15 +++++++++++++++
 app/lib/user_settings_decorator.rb                 |  5 +++++
 app/models/user.rb                                 |  4 ++++
 app/views/home/initial_state.json.rabl             |  1 +
 app/views/settings/preferences/show.html.haml      |  1 +
 config/locales/simple_form.en.yml                  |  1 +
 config/locales/simple_form.fr.yml                  |  1 +
 config/settings.yml                                |  1 +
 spec/lib/user_settings_decorator_spec.rb           |  7 +++++++
 spec/models/user_spec.rb                           |  8 ++++++++
 12 files changed, 57 insertions(+), 2 deletions(-)

(limited to 'spec')

diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 71f5a7c04..a15c26031 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -37,6 +37,7 @@ class Settings::PreferencesController < ApplicationController
       :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/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 08d087da1..54e623d99 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -1,4 +1,5 @@
 import React from 'react';
+import classNames from 'classnames';
 import Switch from 'react-router-dom/Switch';
 import Route from 'react-router-dom/Route';
 import Redirect from 'react-router-dom/Redirect';
@@ -72,12 +73,17 @@ class WrappedRoute extends React.Component {
 
 }
 
-@connect()
+const mapStateToProps = state => ({
+  systemFontUi: state.getIn(['meta', 'system_font_ui']),
+});
+
+@connect(mapStateToProps)
 export default class UI extends React.PureComponent {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     children: PropTypes.node,
+    systemFontUi: PropTypes.bool,
   };
 
   state = {
@@ -176,8 +182,12 @@ export default class UI extends React.PureComponent {
     const { width, draggingOver } = this.state;
     const { children } = this.props;
 
+    const className = classNames('ui', {
+      'system-font': this.props.systemFontUi,
+    });
+
     return (
-      <div className='ui' ref={this.setRef}>
+      <div className={className} ref={this.setRef}>
         <TabsBar />
         <ColumnsAreaContainer singleColumn={isMobile(width)}>
           <WrappedSwitch>
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss
index 0cb271ddd..4da698e81 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/basics.scss
@@ -63,3 +63,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/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index af264bbd5..9c0cb4545 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -21,6 +21,7 @@ class UserSettingsDecorator
     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
@@ -43,6 +44,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/models/user.rb b/app/models/user.rb
index c31a0c644..e2bb3d0ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -91,6 +91,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/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
index e305f8e7a..291ff806b 100644
--- a/app/views/home/initial_state.json.rabl
+++ b/app/views/home/initial_state.json.rabl
@@ -11,6 +11,7 @@ node(:meta) do
     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,
+    system_font_ui: current_account.user.setting_system_font_ui,
   }
 end
 
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 721ce6a21..26fbfdf82 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -44,6 +44,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/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 3e769fb96..d8d3b8a6f 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -38,6 +38,7 @@ en:
         setting_boost_modal: Show confirmation dialog before boosting
         setting_default_privacy: Post privacy
         setting_delete_modal: Show confirmation dialog before deleting a toot
+        setting_system_font_ui: Use system's default font
         severity: Severity
         type: Import type
         username: Username
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index ae4975143..446c56947 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -28,6 +28,7 @@ fr:
         password: Mot de passe
         setting_boost_modal: Afficher un dialogue de confirmation avant de partager
         setting_default_privacy: Confidentialité des statuts
+        setting_system_font_ui: Utiliser la police par défaut du système
         severity: Séverité
         type: Type d'import
         username: Identifiant
diff --git a/config/settings.yml b/config/settings.yml
index 5aea232e7..18b70b51f 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -19,6 +19,7 @@ defaults: &defaults
   boost_modal: false
   auto_play_gif: true
   delete_modal: true
+  system_font_ui: false
   notification_emails:
     follow: false
     reblog: false
diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb
index 66e42fa0e..e1ba56d97 100644
--- a/spec/lib/user_settings_decorator_spec.rb
+++ b/spec/lib/user_settings_decorator_spec.rb
@@ -48,5 +48,12 @@ describe UserSettingsDecorator do
       settings.update(values)
       expect(user.settings['auto_play_gif']).to eq false
     end
+    
+    it 'updates the user settings value for system font in UI' do
+      values = { 'setting_system_font_ui' => '0' }
+
+      settings.update(values)
+      expect(user.settings['system_font_ui']).to eq false
+    end
   end
 end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a6df3fb26..2019ec0f6 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -184,6 +184,14 @@ RSpec.describe User, type: :model do
       expect(user.setting_auto_play_gif).to eq false
     end
   end
+  
+  describe '#setting_system_font_ui' do
+    it 'returns system font ui setting' do
+      user = Fabricate(:user)
+      user.settings[:system_font_ui] = false
+      expect(user.setting_system_font_ui).to eq false
+    end
+  end
 
   describe '#setting_boost_modal' do
     it 'returns boost modal setting' do
-- 
cgit 


From 8b2cad56374b2dbb6e7a445e7917810935c45536 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 7 Jul 2017 04:02:06 +0200
Subject: Refactor JSON templates to be generated with ActiveModelSerializers
 instead of Rabl (#4090)

---
 Gemfile                                            |  1 +
 Gemfile.lock                                       |  9 +++
 .../api/v1/accounts/credentials_controller.rb      |  4 +-
 .../v1/accounts/follower_accounts_controller.rb    |  2 +-
 .../v1/accounts/following_accounts_controller.rb   |  2 +-
 .../api/v1/accounts/relationships_controller.rb    | 11 ++-
 .../api/v1/accounts/search_controller.rb           |  3 +-
 .../api/v1/accounts/statuses_controller.rb         |  5 +-
 app/controllers/api/v1/accounts_controller.rb      | 38 +++------
 app/controllers/api/v1/apps_controller.rb          |  1 +
 app/controllers/api/v1/blocks_controller.rb        |  1 +
 app/controllers/api/v1/favourites_controller.rb    |  5 +-
 .../api/v1/follow_requests_controller.rb           |  1 +
 app/controllers/api/v1/follows_controller.rb       |  2 +-
 app/controllers/api/v1/instances_controller.rb     |  4 +-
 app/controllers/api/v1/media_controller.rb         |  1 +
 app/controllers/api/v1/mutes_controller.rb         |  1 +
 app/controllers/api/v1/notifications_controller.rb |  7 +-
 app/controllers/api/v1/reports_controller.rb       |  3 +-
 app/controllers/api/v1/search_controller.rb        |  3 +-
 .../statuses/favourited_by_accounts_controller.rb  |  2 +-
 .../api/v1/statuses/favourites_controller.rb       |  4 +-
 .../api/v1/statuses/mutes_controller.rb            |  4 +-
 .../statuses/reblogged_by_accounts_controller.rb   |  2 +-
 .../api/v1/statuses/reblogs_controller.rb          |  4 +-
 app/controllers/api/v1/statuses_controller.rb      | 16 ++--
 .../api/v1/timelines/home_controller.rb            |  6 +-
 .../api/v1/timelines/public_controller.rb          |  6 +-
 app/controllers/api/v1/timelines/tag_controller.rb |  6 +-
 app/lib/inline_rabl_scope.rb                       | 17 ----
 app/lib/inline_renderer.rb                         | 36 +++++++--
 app/models/context.rb                              |  5 ++
 app/models/search.rb                               |  5 ++
 app/presenters/account_relationships_presenter.rb  | 15 ++++
 app/presenters/status_relationships_presenter.rb   | 19 +++++
 app/serializers/rest/account_serializer.rb         | 33 ++++++++
 app/serializers/rest/application_serializer.rb     | 14 ++++
 app/serializers/rest/context_serializer.rb         |  6 ++
 app/serializers/rest/instance_serializer.rb        | 30 +++++++
 .../rest/media_attachment_serializer.rb            | 24 ++++++
 app/serializers/rest/notification_serializer.rb    | 12 +++
 app/serializers/rest/preview_card_serializer.rb    | 14 ++++
 app/serializers/rest/relationship_serializer.rb    | 30 +++++++
 app/serializers/rest/report_serializer.rb          |  5 ++
 app/serializers/rest/search_serializer.rb          | 12 +++
 app/serializers/rest/status_serializer.rb          | 93 ++++++++++++++++++++++
 app/services/fan_out_on_write_service.rb           |  2 +-
 app/services/notify_service.rb                     |  2 +-
 app/views/api/v1/accounts/index.rabl               |  2 -
 app/views/api/v1/accounts/relationship.rabl        |  9 ---
 app/views/api/v1/accounts/relationships/index.rabl |  2 -
 app/views/api/v1/accounts/show.rabl                | 12 ---
 app/views/api/v1/accounts/statuses/index.rabl      |  2 -
 app/views/api/v1/apps/create.rabl                  |  4 -
 app/views/api/v1/apps/show.rabl                    |  3 -
 app/views/api/v1/blocks/index.rabl                 |  2 -
 app/views/api/v1/favourites/index.rabl             |  2 -
 app/views/api/v1/follow_requests/index.rabl        |  2 -
 app/views/api/v1/follows/show.rabl                 |  2 -
 app/views/api/v1/instances/show.rabl               | 10 ---
 app/views/api/v1/media/create.rabl                 |  7 --
 app/views/api/v1/mutes/index.rabl                  |  2 -
 app/views/api/v1/notifications/index.rabl          |  2 -
 app/views/api/v1/notifications/show.rabl           | 11 ---
 app/views/api/v1/reports/index.rabl                |  2 -
 app/views/api/v1/reports/show.rabl                 |  2 -
 app/views/api/v1/search/index.rabl                 | 13 ---
 app/views/api/v1/statuses/_media.rabl              |  6 --
 app/views/api/v1/statuses/_mention.rabl            |  4 -
 app/views/api/v1/statuses/_show.rabl               | 29 -------
 app/views/api/v1/statuses/_tags.rabl               |  2 -
 app/views/api/v1/statuses/accounts.rabl            |  2 -
 app/views/api/v1/statuses/card.rabl                |  7 --
 app/views/api/v1/statuses/context.rabl             |  9 ---
 app/views/api/v1/statuses/index.rabl               |  2 -
 app/views/api/v1/statuses/show.rabl                | 15 ----
 app/views/api/v1/timelines/show.rabl               |  2 -
 app/views/home/initial_state.json.rabl             |  4 +-
 app/workers/push_update_worker.rb                  |  2 +-
 spec/lib/inline_rabl_scope_spec.rb                 | 23 ------
 80 files changed, 425 insertions(+), 301 deletions(-)
 delete mode 100644 app/lib/inline_rabl_scope.rb
 create mode 100644 app/models/context.rb
 create mode 100644 app/models/search.rb
 create mode 100644 app/presenters/account_relationships_presenter.rb
 create mode 100644 app/presenters/status_relationships_presenter.rb
 create mode 100644 app/serializers/rest/account_serializer.rb
 create mode 100644 app/serializers/rest/application_serializer.rb
 create mode 100644 app/serializers/rest/context_serializer.rb
 create mode 100644 app/serializers/rest/instance_serializer.rb
 create mode 100644 app/serializers/rest/media_attachment_serializer.rb
 create mode 100644 app/serializers/rest/notification_serializer.rb
 create mode 100644 app/serializers/rest/preview_card_serializer.rb
 create mode 100644 app/serializers/rest/relationship_serializer.rb
 create mode 100644 app/serializers/rest/report_serializer.rb
 create mode 100644 app/serializers/rest/search_serializer.rb
 create mode 100644 app/serializers/rest/status_serializer.rb
 delete mode 100644 app/views/api/v1/accounts/index.rabl
 delete mode 100644 app/views/api/v1/accounts/relationship.rabl
 delete mode 100644 app/views/api/v1/accounts/relationships/index.rabl
 delete mode 100644 app/views/api/v1/accounts/show.rabl
 delete mode 100644 app/views/api/v1/accounts/statuses/index.rabl
 delete mode 100644 app/views/api/v1/apps/create.rabl
 delete mode 100644 app/views/api/v1/apps/show.rabl
 delete mode 100644 app/views/api/v1/blocks/index.rabl
 delete mode 100644 app/views/api/v1/favourites/index.rabl
 delete mode 100644 app/views/api/v1/follow_requests/index.rabl
 delete mode 100644 app/views/api/v1/follows/show.rabl
 delete mode 100644 app/views/api/v1/instances/show.rabl
 delete mode 100644 app/views/api/v1/media/create.rabl
 delete mode 100644 app/views/api/v1/mutes/index.rabl
 delete mode 100644 app/views/api/v1/notifications/index.rabl
 delete mode 100644 app/views/api/v1/notifications/show.rabl
 delete mode 100644 app/views/api/v1/reports/index.rabl
 delete mode 100644 app/views/api/v1/reports/show.rabl
 delete mode 100644 app/views/api/v1/search/index.rabl
 delete mode 100644 app/views/api/v1/statuses/_media.rabl
 delete mode 100644 app/views/api/v1/statuses/_mention.rabl
 delete mode 100644 app/views/api/v1/statuses/_show.rabl
 delete mode 100644 app/views/api/v1/statuses/_tags.rabl
 delete mode 100644 app/views/api/v1/statuses/accounts.rabl
 delete mode 100644 app/views/api/v1/statuses/card.rabl
 delete mode 100644 app/views/api/v1/statuses/context.rabl
 delete mode 100644 app/views/api/v1/statuses/index.rabl
 delete mode 100644 app/views/api/v1/statuses/show.rabl
 delete mode 100644 app/views/api/v1/timelines/show.rabl
 delete mode 100644 spec/lib/inline_rabl_scope_spec.rb

(limited to 'spec')

diff --git a/Gemfile b/Gemfile
index 6ee884a17..95c74eef9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,6 +18,7 @@ gem 'aws-sdk', '~> 2.9'
 gem 'paperclip', '~> 5.1'
 gem 'paperclip-av-transcoder', '~> 0.6'
 
+gem 'active_model_serializers', '~> 0.10'
 gem 'addressable', '~> 2.5'
 gem 'bootsnap'
 gem 'browser'
diff --git a/Gemfile.lock b/Gemfile.lock
index f0156529c..71f83f736 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,6 +24,11 @@ GEM
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.3)
+    active_model_serializers (0.10.6)
+      actionpack (>= 4.1, < 6)
+      activemodel (>= 4.1, < 6)
+      case_transform (>= 0.2)
+      jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
     active_record_query_trace (1.5.4)
     activejob (5.1.2)
       activesupport (= 5.1.2)
@@ -101,6 +106,8 @@ GEM
       rack (>= 1.0.0)
       rack-test (>= 0.5.4)
       xpath (~> 2.0)
+    case_transform (0.2)
+      activesupport
     chunky_png (1.3.8)
     cld3 (3.1.3)
       ffi (>= 1.1.0, < 1.10.0)
@@ -200,6 +207,7 @@ GEM
       terminal-table (>= 1.5.1)
     jmespath (1.3.1)
     json (2.1.0)
+    jsonapi-renderer (0.1.2)
     kaminari (1.0.1)
       activesupport (>= 4.1.0)
       kaminari-actionview (= 1.0.1)
@@ -476,6 +484,7 @@ PLATFORMS
   ruby
 
 DEPENDENCIES
+  active_model_serializers (~> 0.10)
   active_record_query_trace (~> 1.5)
   addressable (~> 2.5)
   annotate (~> 2.7)
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 1cf52ff10..8ee9a2416 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::AccountSerializer
   end
 
   def update
     current_account.update!(account_params)
     @account = current_account
-    render 'api/v1/accounts/show'
+    render json: @account, serializer: REST::AccountSerializer
   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..1353682ea 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -6,7 +6,8 @@ class Api::V1::SearchController < Api::BaseController
   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/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/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/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/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/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/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/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..9b07a686e
--- /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
+    medium_url(object.id)
+  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..97fadf32e
--- /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 :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/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/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/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
index 291ff806b..c428a5a1f 100644
--- a/app/views/home/initial_state.json.rabl
+++ b/app/views/home/initial_state.json.rabl
@@ -24,8 +24,8 @@ 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[current_account.id] = ActiveModelSerializers::SerializableResource.new(current_account, serializer: REST::AccountSerializer)
+  store[@admin.id] = ActiveModelSerializers::SerializableResource.new(@admin, serializer: REST::AccountSerializer) unless @admin.nil?
   store
 end
 
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
diff --git a/spec/lib/inline_rabl_scope_spec.rb b/spec/lib/inline_rabl_scope_spec.rb
deleted file mode 100644
index 3fff176e4..000000000
--- a/spec/lib/inline_rabl_scope_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe InlineRablScope do
-  describe '#current_account' do
-    it 'returns the given account' do
-      account = Fabricate(:account)
-      expect(InlineRablScope.new(account).current_account).to eq account
-    end
-  end
-
-  describe '#current_user' do
-    it 'returns nil if the given account is nil' do
-      expect(InlineRablScope.new(nil).current_user).to eq nil
-    end
-
-    it 'returns user of account if the given account is not nil' do
-      user = Fabricate(:user)
-      expect(InlineRablScope.new(user.account).current_user).to eq user
-    end
-  end
-end
-- 
cgit 


From 00df69bc89f1b5ffdf290bde8359b3854e2b1395 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 7 Jul 2017 23:25:15 +0200
Subject: Fix #4058 - Use a long-lived cookie to keep track of user-level
 sessions (#4091)

* Fix #4058 - Use a long-lived cookie to keep track of user-level sessions

* Fix tests, smooth migrate from previous session-based identifier
---
 app/controllers/application_controller.rb |  2 +-
 config/initializers/devise.rb             | 20 ++++++++++++++++----
 spec/rails_helper.rb                      | 11 ++++++++---
 3 files changed, 25 insertions(+), 8 deletions(-)

(limited to 'spec')

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/config/initializers/devise.rb b/config/initializers/devise.rb
index d51471d30..bf61ea0ea 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -1,17 +1,29 @@
 Warden::Manager.after_set_user except: :fetch do |user, warden|
-  SessionActivation.deactivate warden.raw_session['auth_id']
-  warden.raw_session['auth_id'] = user.activate_session(warden.request)
+  SessionActivation.deactivate warden.cookies.signed['_session_id']
+
+  warden.cookies.signed['_session_id'] = {
+    value: user.activate_session(warden.request),
+    expires: 1.year.from_now,
+    httponly: true,
+  }
 end
 
 Warden::Manager.after_fetch do |user, warden|
-  unless user.session_active?(warden.raw_session['auth_id'])
+  if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'])
+    warden.cookies.signed['_session_id'] = {
+      value: warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'],
+      expires: 1.year.from_now,
+      httponly: true,
+    }
+  else
     warden.logout
     throw :warden, message: :unauthenticated
   end
 end
 
 Warden::Manager.before_logout do |_, warden|
-  SessionActivation.deactivate warden.raw_session['auth_id']
+  SessionActivation.deactivate warden.cookies.signed['_session_id']
+  warden.cookies.delete('_session_id')
 end
 
 Devise.setup do |config|
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 9a4c8fd3c..4f7399505 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -20,11 +20,16 @@ Sidekiq::Logging.logger = nil
 Devise::Test::ControllerHelpers.module_eval do
   alias_method :original_sign_in, :sign_in
 
-  def sign_in(resource, deprecated = nil, scope: nil)
+  def sign_in(resource, _deprecated = nil, scope: nil)
     original_sign_in(resource, scope: scope)
 
-    SessionActivation.deactivate warden.raw_session["auth_id"]
-    warden.raw_session["auth_id"] = resource.activate_session(warden.request)
+    SessionActivation.deactivate warden.cookies.signed['_session_id']
+
+    warden.cookies.signed['_session_id'] = {
+      value: resource.activate_session(warden.request),
+      expires: 1.year.from_now,
+      httponly: true,
+    }
   end
 end
 
-- 
cgit 


From 864e3f8d9ca652e10a28bddbb0d0df629d2849d4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sat, 8 Jul 2017 14:51:05 +0200
Subject: Replace OEmbed and initial state Rabl templates with serializers
 (#4110)

* Replace OEmbed Rabl template with serializer

* Replace initial state rabl with serializer
---
 app/controllers/api/oembed_controller.rb    |  3 +-
 app/controllers/home_controller.rb          | 17 ++++++---
 app/presenters/initial_state_presenter.rb   |  5 +++
 app/serializers/initial_state_serializer.rb | 39 ++++++++++++++++++++
 app/serializers/oembed_serializer.rb        | 56 +++++++++++++++++++++++++++++
 app/views/api/oembed/show.json.rabl         | 14 --------
 app/views/home/index.html.haml              |  2 +-
 app/views/home/initial_state.json.rabl      | 38 --------------------
 spec/controllers/home_controller_spec.rb    | 41 ++++-----------------
 9 files changed, 121 insertions(+), 94 deletions(-)
 create mode 100644 app/presenters/initial_state_presenter.rb
 create mode 100644 app/serializers/initial_state_serializer.rb
 create mode 100644 app/serializers/oembed_serializer.rb
 delete mode 100644 app/views/api/oembed/show.json.rabl
 delete mode 100644 app/views/home/initial_state.json.rabl

(limited to 'spec')

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/home_controller.rb b/app/controllers/home_controller.rb
index 6209a3ae9..218da6906 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,14 @@ 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
+    state = InitialStatePresenter.new(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))
+
+    serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer)
+    @initial_state_json   = serializable_resource.to_json
+  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/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
new file mode 100644
index 000000000..84f9e23a6
--- /dev/null
+++ b/app/serializers/initial_state_serializer.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class InitialStateSerializer < ActiveModel::Serializer
+  attributes :meta, :compose, :accounts,
+             :media_attachments, :settings
+
+  def meta
+    {
+      streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
+      access_token: object.token,
+      locale: I18n.locale,
+      domain: Rails.configuration.x.local_domain,
+      me: object.current_account.id,
+      admin: object.admin&.id,
+      boost_modal: object.current_account.user.setting_boost_modal,
+      delete_modal: object.current_account.user.setting_delete_modal,
+      auto_play_gif: object.current_account.user.setting_auto_play_gif,
+      system_font_ui: object.current_account.user.setting_system_font_ui,
+    }
+  end
+
+  def compose
+    {
+      me: object.current_account.id,
+      default_privacy: object.current_account.user.setting_default_privacy,
+    }
+  end
+
+  def accounts
+    store = {}
+    store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer)
+    store[object.admin.id]           = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) unless object.admin.nil?
+    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/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/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 c428a5a1f..000000000
--- a/app/views/home/initial_state.json.rabl
+++ /dev/null
@@ -1,38 +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,
-    system_font_ui: current_account.user.setting_system_font_ui,
-  }
-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] = ActiveModelSerializers::SerializableResource.new(current_account, serializer: REST::AccountSerializer)
-  store[@admin.id] = ActiveModelSerializers::SerializableResource.new(@admin, serializer: REST::AccountSerializer) 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/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
index cc1dbe5a1..d44d720b1 100644
--- a/spec/controllers/home_controller_spec.rb
+++ b/spec/controllers/home_controller_spec.rb
@@ -23,41 +23,14 @@ RSpec.describe HomeController, type: :controller do
         expect(assigns(:body_classes)).to eq 'app-body'
       end
 
-      it 'assigns @token' do
-        app = Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri)
-        allow(Doorkeeper.configuration).to receive(:access_token_expires_in).and_return(42)
-
-        subject
-        token = Doorkeeper::AccessToken.find_by(token: assigns(:token))
-
-        expect(token.application).to eq app
-        expect(token.resource_owner_id).to eq user.id
-        expect(token.scopes).to eq Doorkeeper::OAuth::Scopes.from_string('read write follow')
-        expect(token.expires_in_seconds).to eq 42
-        expect(token.use_refresh_token?).to eq false
-      end
-
-      it 'assigns @web_settings for {} if not available' do
-        subject
-        expect(assigns(:web_settings)).to eq({})
-      end
-
-      it 'assigns @web_settings for Web::Setting if available' do
-        setting = Fabricate('Web::Setting', data: '{"home":{}}', user: user)
-        subject
-        expect(assigns(:web_settings)).to eq setting.data
-      end
-
-      it 'assigns @admin' do
-        admin = Fabricate(:account)
-        Setting.site_contact_username = admin.username
-        subject
-        expect(assigns(:admin)).to eq admin
-      end
-
-      it 'assigns streaming_api_base_url' do
+      it 'assigns @initial_state_json' do
         subject
-        expect(assigns(:streaming_api_base_url)).to eq 'ws://localhost:4000'
+        initial_state_json = json_str_to_hash(assigns(:initial_state_json))
+        expect(initial_state_json[:meta]).to_not be_nil
+        expect(initial_state_json[:compose]).to_not be_nil
+        expect(initial_state_json[:accounts]).to_not be_nil
+        expect(initial_state_json[:settings]).to_not be_nil
+        expect(initial_state_json[:media_attachments]).to_not be_nil
       end
     end
   end
-- 
cgit 


From 007ab330e6ffb1e07995d4e306473d457043e2eb Mon Sep 17 00:00:00 2001
From: nullkal <nullkal@users.noreply.github.com>
Date: Sun, 9 Jul 2017 05:44:31 +0900
Subject: Use charlock_holmes instead of nkf at FetchLinkCardService (#4080)

* Specs for language detection

* Use CharlockHolmes instead of NKF

* Correct mistakes

* Correct style

* Set hint_enc instead of falling back and strip_tags

* Improve specs

* Add dependencies
---
 .travis.yml                                        |  1 +
 Aptfile                                            |  1 +
 Dockerfile                                         |  1 +
 Gemfile                                            |  1 +
 Gemfile.lock                                       |  2 ++
 Vagrantfile                                        |  1 +
 app/services/fetch_link_card_service.rb            |  8 ++++++--
 spec/fixtures/requests/koi8-r.txt                  | 20 +++++++++++++++++++
 spec/fixtures/requests/sjis.txt                    |  4 ++--
 spec/fixtures/requests/sjis_with_wrong_charset.txt | 20 +++++++++++++++++++
 spec/services/fetch_link_card_service_spec.rb      | 23 ++++++++++++++++++++++
 11 files changed, 78 insertions(+), 4 deletions(-)
 create mode 100644 spec/fixtures/requests/koi8-r.txt
 create mode 100644 spec/fixtures/requests/sjis_with_wrong_charset.txt

(limited to 'spec')

diff --git a/.travis.yml b/.travis.yml
index 4bb332666..4d4dc0893 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,6 +32,7 @@ addons:
     - g++-6
     - libprotobuf-dev
     - protobuf-compiler
+    - libicu-dev
 
 rvm:
   - 2.3.4
diff --git a/Aptfile b/Aptfile
index 0456343ef..3af0956e3 100644
--- a/Aptfile
+++ b/Aptfile
@@ -3,3 +3,4 @@ libprotobuf-dev
 ffmpeg
 libxdamage1
 libxfixes3
+libicu-dev
diff --git a/Dockerfile b/Dockerfile
index 7033cddd4..97a691393 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,6 +25,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
     ffmpeg \
     file \
     git \
+    icu-dev \
     imagemagick@edge \
     libpq \
     libxml2 \
diff --git a/Gemfile b/Gemfile
index 95c74eef9..b52685cba 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,6 +22,7 @@ gem 'active_model_serializers', '~> 0.10'
 gem 'addressable', '~> 2.5'
 gem 'bootsnap'
 gem 'browser'
+gem 'charlock_holmes', '~> 0.7.3'
 gem 'cld3', '~> 3.1'
 gem 'devise', '~> 4.2'
 gem 'devise-two-factor', '~> 3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 71f83f736..de0d6a107 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -106,6 +106,7 @@ GEM
       rack (>= 1.0.0)
       rack-test (>= 0.5.4)
       xpath (~> 2.0)
+    charlock_holmes (0.7.3)
     case_transform (0.2)
       activesupport
     chunky_png (1.3.8)
@@ -501,6 +502,7 @@ DEPENDENCIES
   capistrano-rbenv (~> 2.1)
   capistrano-yarn (~> 2.0)
   capybara (~> 2.14)
+  charlock_holmes (~> 0.7.3)
   cld3 (~> 3.1)
   climate_control (~> 0.2)
   devise (~> 4.2)
diff --git a/Vagrantfile b/Vagrantfile
index 1f56fcfb3..cbe6623b3 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -37,6 +37,7 @@ sudo apt-get install \
   yarn \
   libprotobuf-dev \
   libreadline-dev \
+  libicu-dev \
   -y
 
 # Install rvm
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 8ddaa2bf4..6ef3abb66 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'nkf'
 
 class FetchLinkCardService < BaseService
   include HttpHelper
@@ -86,7 +85,12 @@ class FetchLinkCardService < BaseService
     return if response.code != 200 || response.mime_type != 'text/html'
 
     html = response.to_s
-    page = Nokogiri::HTML(html, nil, NKF.guess(html).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/spec/fixtures/requests/koi8-r.txt b/spec/fixtures/requests/koi8-r.txt
new file mode 100644
index 000000000..d4242af01
--- /dev/null
+++ b/spec/fixtures/requests/koi8-r.txt
@@ -0,0 +1,20 @@
+HTTP/1.1 200 OK
+Server: nginx/1.11.10
+Date: Tue, 04 Jul 2017 16:43:39 GMT
+Content-Type: text/html
+Content-Length: 273
+Connection: keep-alive
+Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT
+Accept-Ranges: bytes
+
+<HTML>
+<HEAD>
+  <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac">
+  <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=koi8-r">
+  <TITLE>������� ��������� ������ �� XVI ��. ���������� ������� ������������.</TITLE>
+</HEAD>
+<BODY>
+<P><CENTER><B><FONT SIZE="+2">������� ��������� ������ �� XVI ��. ���������� ������� ������������.</FONT></B><BR>
+<HR><BR>
+</BODY>
+</HTML>
diff --git a/spec/fixtures/requests/sjis.txt b/spec/fixtures/requests/sjis.txt
index 9041aa25d..faf18d35c 100644
--- a/spec/fixtures/requests/sjis.txt
+++ b/spec/fixtures/requests/sjis.txt
@@ -11,10 +11,10 @@ Accept-Ranges: bytes
 <HEAD>
   <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac">
   <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=x-sjis">
-  <TITLE>JSIS�̃y�[�W</TITLE>
+  <TITLE>SJIS�̃y�[�W</TITLE>
 </HEAD>
 <BODY>
-<P><CENTER><B><FONT SIZE="+2">SJIS�̃y�[�W</FONT></B><BR>
+<P><CENTER><B><FONT SIZE="+2">�������N�܂��Ă�����L�O�l���Ă��̂̎��ł�����ł��B�������ԂɈӖ��҂͐������ǂ�Ȕ���܂����܂ł��\���グ�����������邽�ɂ͎Q�l�A�邽��������A�����ɂ���������܂��Ȃ��B�����炢���Ȃ��̂͂ǂ����㌎���ł��邾�����������B�������ĉ��c����ɔ��R�K�������ɉ]���ł�����͂����������͂��Ȃ����w�}���Ƃ������o����Ȃ�����Ȃ���āA���͎̐̂������͉A��{�炩��A�v������̂��̂������̂��‚�������ɂ���]�ƌ����΂���man�ɂ������֎Q��悤�ɓ����ɂ����������łȂ�̂ŁA���������\���ɕς����Ă���ł����ōl�������B�������Ⴆ�΂��������Ƃǂ܂���̂����ۂނ�݂Ƃ���ł��āA���̎����ł͐\����ĂƂ��Đ��Ԃɕ��ׂ̂ɍs���Ȃ����ȁB</FONT></B><BR>
 <HR><BR>
 </BODY>
 </HTML>
diff --git a/spec/fixtures/requests/sjis_with_wrong_charset.txt b/spec/fixtures/requests/sjis_with_wrong_charset.txt
new file mode 100644
index 000000000..456750c6b
--- /dev/null
+++ b/spec/fixtures/requests/sjis_with_wrong_charset.txt
@@ -0,0 +1,20 @@
+HTTP/1.1 200 OK
+Server: nginx/1.11.10
+Date: Tue, 04 Jul 2017 16:43:39 GMT
+Content-Type: text/html; charset=utf-8
+Content-Length: 273
+Connection: keep-alive
+Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT
+Accept-Ranges: bytes
+
+<HTML>
+<HEAD>
+  <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac">
+  <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=x-sjis">
+  <TITLE>SJIS�̃y�[�W</TITLE>
+</HEAD>
+<BODY>
+<P><CENTER><B><FONT SIZE="+2">�������N�܂��Ă�����L�O�l���Ă��̂̎��ł�����ł��B�������ԂɈӖ��҂͐������ǂ�Ȕ���܂����܂ł��\���グ�����������邽�ɂ͎Q�l�A�邽��������A�����ɂ���������܂��Ȃ��B�����炢���Ȃ��̂͂ǂ����㌎���ł��邾�����������B�������ĉ��c����ɔ��R�K�������ɉ]���ł�����͂����������͂��Ȃ����w�}���Ƃ������o����Ȃ�����Ȃ���āA���͎̐̂������͉A��{�炩��A�v������̂��̂������̂��‚�������ɂ���]�ƌ����΂���man�ɂ������֎Q��悤�ɓ����ɂ����������łȂ�̂ŁA���������\���ɕς����Ă���ł����ōl�������B�������Ⴆ�΂��������Ƃǂ܂���̂����ۂނ�݂Ƃ���ł��āA���̎����ł͐\����ĂƂ��Đ��Ԃɕ��ׂ̂ɍs���Ȃ����ȁB</FONT></B><BR>
+<HR><BR>
+</BODY>
+</HTML>
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 7d7f8e748..698eb0324 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -8,6 +8,10 @@ RSpec.describe FetchLinkCardService do
     stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt'))
     stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
     stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt'))
+    stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
+    stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt'))
+    stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
+    stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
     stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404)
 
     subject.call(status)
@@ -27,6 +31,25 @@ RSpec.describe FetchLinkCardService do
 
       it 'works with SJIS' do
         expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once
+        expect(status.preview_card.title).to eq("SJISのページ")
+      end
+    end
+
+    context do
+      let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') }
+
+      it 'works with SJIS even with wrong charset header' do
+        expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once
+        expect(status.preview_card.title).to eq("SJISのページ")
+      end
+    end
+
+    context do
+      let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') }
+
+      it 'works with koi8-r' do
+        expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once
+        expect(status.preview_card.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.")
       end
     end
   end
-- 
cgit 


From 2b9721d1b38319d70bed98e76a0fe1d648780298 Mon Sep 17 00:00:00 2001
From: Yamagishi Kazutoshi <ykzts@desire.sh>
Date: Mon, 10 Jul 2017 21:00:32 +0900
Subject: Add setting a always mark media as sensitive (#4136)

---
 app/controllers/settings/preferences_controller.rb    | 1 +
 app/javascript/mastodon/reducers/compose.js           | 7 +++++++
 app/lib/user_settings_decorator.rb                    | 5 +++++
 app/models/user.rb                                    | 4 ++++
 app/serializers/initial_state_serializer.rb           | 1 +
 app/serializers/rest/credential_account_serializer.rb | 1 +
 app/views/settings/preferences/show.html.haml         | 2 ++
 config/locales/simple_form.en.yml                     | 1 +
 spec/lib/user_settings_decorator_spec.rb              | 7 +++++++
 9 files changed, 29 insertions(+)

(limited to 'spec')

diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index a15c26031..cac5b0ba8 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -34,6 +34,7 @@ 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,
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index d0b47a85c..752377739 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -45,6 +45,7 @@ const initialState = Immutable.Map({
   suggestions: Immutable.List(),
   me: null,
   default_privacy: 'public',
+  default_sensitive: false,
   resetFileKey: Math.floor((Math.random() * 0x10000)),
   idempotencyKey: null,
 });
@@ -75,6 +76,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);
@@ -82,6 +85,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);
+    }
   });
 };
 
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 9c0cb4545..e0e92b19d 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -18,6 +18,7 @@ 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
@@ -36,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
diff --git a/app/models/user.rb b/app/models/user.rb
index e2bb3d0ed..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
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 84f9e23a6..49ff9e377 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -23,6 +23,7 @@ class InitialStateSerializer < ActiveModel::Serializer
     {
       me: object.current_account.id,
       default_privacy: object.current_account.user.setting_default_privacy,
+      default_sensitive: object.current_account.user.setting_default_sensitive,
     }
   end
 
diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb
index 094b831c9..870d8b71f 100644
--- a/app/serializers/rest/credential_account_serializer.rb
+++ b/app/serializers/rest/credential_account_serializer.rb
@@ -7,6 +7,7 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer
     user = object.user
     {
       privacy: user.setting_default_privacy,
+      sensitive: user.setting_default_sensitive,
       note: object.note,
     }
   end
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 26fbfdf82..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
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index d8d3b8a6f..fc5ab5ec8 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -37,6 +37,7 @@ en:
         setting_auto_play_gif: Auto-play animated GIFs
         setting_boost_modal: Show confirmation dialog before boosting
         setting_default_privacy: Post privacy
+        setting_default_sensitive: Always mark media as sensitive
         setting_delete_modal: Show confirmation dialog before deleting a toot
         setting_system_font_ui: Use system's default font
         severity: Severity
diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb
index e1ba56d97..a67487779 100644
--- a/spec/lib/user_settings_decorator_spec.rb
+++ b/spec/lib/user_settings_decorator_spec.rb
@@ -28,6 +28,13 @@ describe UserSettingsDecorator do
       expect(user.settings['default_privacy']).to eq 'public'
     end
 
+    it 'updates the user settings value for sensitive' do
+      values = { 'setting_default_sensitive' => '1' }
+
+      settings.update(values)
+      expect(user.settings['default_sensitive']).to eq true
+    end
+
     it 'updates the user settings value for boost modal' do
       values = { 'setting_boost_modal' => '1' }
 
-- 
cgit 


From 7a889a8e125a03e109b225cd83b0abcbdc76d95b Mon Sep 17 00:00:00 2001
From: STJrInuyasha <MattWCSTRFAN@gmail.com>
Date: Mon, 10 Jul 2017 09:05:06 -0700
Subject: Remote following success page (#4129)

* Added a success page to remote following
Includes follow-through links to web (the old redirect target) and back to the remote user's profile

* Use Account.new in spec instead of a fake with only id
(fixes spec)

* Fabricate(:account) over Account.new

* Remove self from the success text
(and all HTML with it)
---
 app/controllers/authorize_follows_controller.rb       |  2 +-
 app/javascript/styles/forms.scss                      |  9 +++++++++
 app/views/authorize_follows/success.html.haml         | 16 ++++++++++++++++
 config/locales/en.yml                                 |  6 ++++++
 spec/controllers/authorize_follows_controller_spec.rb |  4 ++--
 5 files changed, 34 insertions(+), 3 deletions(-)
 create mode 100644 app/views/authorize_follows/success.html.haml

(limited to 'spec')

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/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index 7a181f36b..414dc4fe8 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -375,3 +375,12 @@ code {
     width: 50%;
   }
 }
+
+.post-follow-actions {
+  text-align: center;
+  color: $ui-primary-color;
+
+  div {
+    margin-bottom: 4px;
+  }
+}
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/config/locales/en.yml b/config/locales/en.yml
index 60e192491..8bb893d1c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -221,6 +221,12 @@ en:
   authorize_follow:
     error: Unfortunately, there was an error looking up the remote account
     follow: Follow
+    following: 'Success! You are now following:'
+    follow_request: 'You have sent a follow request to:'
+    post_follow:
+      web: Go to web
+      return: Return to the user's profile
+      close: Or, you can just close this window.
     prompt_html: 'You (<strong>%{self}</strong>) have requested to follow:'
     title: Follow %{acct}
   datetime:
diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb
index b801aa661..26e46a23c 100644
--- a/spec/controllers/authorize_follows_controller_spec.rb
+++ b/spec/controllers/authorize_follows_controller_spec.rb
@@ -94,7 +94,7 @@ describe AuthorizeFollowsController do
       end
 
       it 'follows account when found' do
-        target_account = double(id: '123')
+        target_account = Fabricate(:account)
         result_account = double(target_account: target_account)
         service = double
         allow(FollowService).to receive(:new).and_return(service)
@@ -103,7 +103,7 @@ describe AuthorizeFollowsController do
         post :create, params: { acct: 'acct:user@hostname' }
 
         expect(service).to have_received(:call).with(account, 'user@hostname')
-        expect(response).to redirect_to(web_url('accounts/123'))
+        expect(response).to render_template(:success)
       end
     end
   end
-- 
cgit 


From 7bacdd718a143f54f47ddc3afa39504636be65c0 Mon Sep 17 00:00:00 2001
From: "Akihiko Odaki (@fn_aki@pawoo.net)" <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Tue, 11 Jul 2017 08:00:01 +0900
Subject: Fix PrecomputeFeedService for filtered statuses (#4148)

---
 app/services/precompute_feed_service.rb       |  4 ++--
 spec/services/precompute_feed_service_spec.rb | 12 ++++++++++++
 2 files changed, 14 insertions(+), 2 deletions(-)

(limited to 'spec')

diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index a32ba1dae..85635a008 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -13,7 +13,7 @@ class PrecomputeFeedService < BaseService
   attr_reader :account
 
   def populate_feed
-    pairs = statuses.reverse_each.map(&method(:process_status))
+    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?
@@ -22,7 +22,7 @@ class PrecomputeFeedService < BaseService
   end
 
   def process_status(status)
-    [status.id, status.reblog? ? status.reblog_of_id : status.id] unless status_filtered?(status)
+    [status.id, status.reblog? ? status.reblog_of_id : status.id]
   end
 
   def status_filtered?(status)
diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb
index e2294469c..dbd08ac1b 100644
--- a/spec/services/precompute_feed_service_spec.rb
+++ b/spec/services/precompute_feed_service_spec.rb
@@ -23,5 +23,17 @@ RSpec.describe PrecomputeFeedService do
       account = Fabricate(:account)
       subject.call(account)
     end
+
+    it 'filters statuses' do
+      account = Fabricate(:account)
+      muted_account = Fabricate(:account)
+      Fabricate(:mute, account: account, target_account: muted_account)
+      reblog = Fabricate(:status, account: muted_account)
+      status = Fabricate(:status, account: account, reblog: reblog)
+
+      subject.call(account)
+
+      expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil
+    end
   end
 end
-- 
cgit 


From cc68d1945b42459a787e0ce216e8f49393eeb197 Mon Sep 17 00:00:00 2001
From: Sorin Davidoi <sorin.davidoi@gmail.com>
Date: Tue, 11 Jul 2017 01:00:14 +0200
Subject: refactor: Rewrite immutablejs import statements using destructuring
 (#4147)

---
 app/javascript/mastodon/actions/notifications.js   |  4 +--
 app/javascript/mastodon/actions/store.js           |  6 ++--
 app/javascript/mastodon/actions/timelines.js       | 10 +++----
 .../mastodon/features/account_timeline/index.js    |  4 +--
 .../mastodon/features/notifications/index.js       |  4 +--
 .../containers/status_check_box_container.js       |  4 +--
 .../features/ui/components/onboarding_modal.js     |  4 +--
 .../features/ui/components/report_modal.js         |  4 +--
 .../ui/containers/status_list_container.js         |  6 ++--
 app/javascript/mastodon/reducers/accounts.js       |  6 ++--
 .../mastodon/reducers/accounts_counters.js         |  8 +++---
 app/javascript/mastodon/reducers/alerts.js         |  6 ++--
 app/javascript/mastodon/reducers/cards.js          |  6 ++--
 app/javascript/mastodon/reducers/compose.js        | 18 ++++++------
 app/javascript/mastodon/reducers/contexts.js       | 18 ++++++------
 .../mastodon/reducers/media_attachments.js         |  4 +--
 app/javascript/mastodon/reducers/meta.js           |  4 +--
 app/javascript/mastodon/reducers/notifications.js  | 14 +++++-----
 app/javascript/mastodon/reducers/relationships.js  |  6 ++--
 app/javascript/mastodon/reducers/reports.js        | 16 +++++------
 app/javascript/mastodon/reducers/search.js         | 16 +++++------
 app/javascript/mastodon/reducers/settings.js       | 30 ++++++++++----------
 app/javascript/mastodon/reducers/status_lists.js   | 10 +++----
 app/javascript/mastodon/reducers/statuses.js       |  6 ++--
 app/javascript/mastodon/reducers/timelines.js      | 24 ++++++++--------
 app/javascript/mastodon/reducers/user_lists.js     | 32 +++++++++++-----------
 app/javascript/mastodon/selectors/index.js         |  6 ++--
 spec/javascript/components/display_name.test.js    |  6 ++--
 28 files changed, 141 insertions(+), 141 deletions(-)

(limited to 'spec')

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 efdb0771a..0597d265e 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -1,11 +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/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/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 2f545fa4a..c5853d3ba 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'))));
 
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/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
index 189bd8665..3d59785e2 100644
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -10,7 +10,7 @@ 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 = () => { };
 
@@ -48,7 +48,7 @@ const PageTwo = ({ me }) => (
       </div>
       <ComposeForm
         text='Awoo! #introductions'
-        suggestions={Immutable.List()}
+        suggestions={ImmutableList()}
         mentionedDomains={[]}
         spoiler={false}
         onChange={noop}
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/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/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 752377739..a92b5aa23 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -24,10 +24,10 @@ 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,
   sensitive: false,
   spoiler: false,
@@ -40,9 +40,9 @@ 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_privacy: 'public',
   default_sensitive: false,
@@ -51,7 +51,7 @@ const initialState = Immutable.Map({
 });
 
 function statusToTextMentions(state, status) {
-  let set = Immutable.OrderedSet([]);
+  let set = ImmutableOrderedSet([]);
   let me  = state.get('me');
 
   if (status.getIn(['account', 'id']) !== me) {
@@ -111,7 +111,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());
   });
@@ -206,7 +206,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:
@@ -219,9 +219,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/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 ddad7a4fc..dd2d76ec0 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -1,39 +1,39 @@
 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,
 
-  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,
@@ -41,20 +41,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: {} },
@@ -82,7 +82,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/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js
index d6dc7edc0..ad9288d4d 100644
--- a/spec/javascript/components/display_name.test.js
+++ b/spec/javascript/components/display_name.test.js
@@ -1,12 +1,12 @@
 import { expect } from 'chai';
 import { render } from 'enzyme';
-import Immutable  from 'immutable';
+import { fromJS }  from 'immutable';
 import React from 'react';
 import DisplayName from '../../../app/javascript/mastodon/components/display_name';
 
 describe('<DisplayName />', () => {
   it('renders display name + account name', () => {
-    const account = Immutable.fromJS({
+    const account = fromJS({
       username: 'bar',
       acct: 'bar@baz',
       display_name: 'Foo',
@@ -16,7 +16,7 @@ describe('<DisplayName />', () => {
   });
 
   it('renders the username + account name if display name is empty', () => {
-    const account = Immutable.fromJS({
+    const account = fromJS({
       username: 'bar',
       acct: 'bar@baz',
       display_name: '',
-- 
cgit 


From e19eefe219c46ea9f763d0279029f03c5cf4554f Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 11 Jul 2017 15:27:59 +0200
Subject: Redesign the landing page, mount public timeline on it (#4122)

* Redesign the landing page, mount public timeline on it

* Adjust the standalone mounted component to the lacking of router

* Adjust auth layout pages to new design

* Fix tests

* Standalone public timeline polling every 5 seconds

* Remove now obsolete translations

* Add responsive design for new landing page

* Address reviews

* Add floating clouds behind frontpage form

* Use access token from public page when available

* Fix mentions and hashtags links, cursor on status content in standalone mode

* Add footer link to source code

* Fix errors on pages that don't embed the component, use classnames

* Fix tests

* Change anonymous autoPlayGif default to false

* When gif autoplay is disabled, hover to play

* Add option to hide the timeline preview

* Slightly improve alt layout

* Add elephant friend to new frontpage

* Display "back to mastodon" in place of "login" when logged in on frontpage

* Change polling time to 3s
---
 app/controllers/about_controller.rb                |  13 +-
 app/controllers/admin/settings_controller.rb       |   9 +-
 app/controllers/home_controller.rb                 |  16 +-
 .../fonts/montserrat/Montserrat-Medium.ttf         | Bin 0 -> 192488 bytes
 app/javascript/images/cloud2.png                   | Bin 0 -> 4973 bytes
 app/javascript/images/cloud3.png                   | Bin 0 -> 5860 bytes
 app/javascript/images/cloud4.png                   | Bin 0 -> 5273 bytes
 app/javascript/images/elephant-fren.png            | Bin 0 -> 40859 bytes
 app/javascript/images/logo.svg                     |   2 +-
 .../mastodon/components/dropdown_menu.js           |  19 +-
 .../mastodon/components/media_gallery.js           |  38 +-
 app/javascript/mastodon/components/permalink.js    |   4 +-
 app/javascript/mastodon/components/status.js       |   8 +-
 .../mastodon/components/status_action_bar.js       |  11 +-
 .../mastodon/components/status_content.js          |  17 +-
 app/javascript/mastodon/components/video_player.js |  22 +-
 .../mastodon/containers/timeline_container.js      |  39 ++
 .../features/standalone/public_timeline/index.js   |  76 ++++
 app/javascript/packs/public.js                     |  10 +
 app/javascript/styles/about.scss                   | 448 ++++++++++++++++++---
 app/javascript/styles/basics.scss                  |   7 +-
 app/javascript/styles/boost.scss                   |   4 +
 app/javascript/styles/components.scss              |  32 +-
 app/javascript/styles/containers.scss              |  48 +--
 app/javascript/styles/fonts/montserrat.scss        |   8 +
 app/javascript/styles/forms.scss                   |  39 +-
 app/presenters/instance_presenter.rb               |   1 +
 app/serializers/initial_state_serializer.rb        |  35 +-
 app/views/about/_features.html.haml                |  25 ++
 app/views/about/_registration.html.haml            |  20 +-
 app/views/about/show.html.haml                     | 120 +++---
 app/views/admin/settings/edit.html.haml            |  43 +-
 app/views/auth/registrations/new.html.haml         |   6 +-
 app/views/layouts/auth.html.haml                   |   3 +-
 config/locales/ar.yml                              |  13 +-
 config/locales/bg.yml                              |  13 +-
 config/locales/ca.yml                              |  17 +-
 config/locales/de.yml                              |  13 -
 config/locales/en.yml                              |  43 +-
 config/locales/eo.yml                              |  23 +-
 config/locales/es.yml                              |  13 +-
 config/locales/fa.yml                              |  13 -
 config/locales/fi.yml                              |  13 +-
 config/locales/fr.yml                              |  15 +-
 config/locales/he.yml                              |  13 -
 config/locales/hr.yml                              |  13 +-
 config/locales/id.yml                              |  13 -
 config/locales/io.yml                              |  13 -
 config/locales/it.yml                              |  13 +-
 config/locales/ja.yml                              |  15 +-
 config/locales/ko.yml                              |  19 +-
 config/locales/nl.yml                              |  21 +-
 config/locales/no.yml                              |  13 -
 config/locales/oc.yml                              |  17 +-
 config/locales/pl.yml                              |  19 +-
 config/locales/pt-BR.yml                           |  13 -
 config/locales/pt.yml                              |  13 -
 config/locales/ru.yml                              |  13 -
 config/locales/th.yml                              |  13 -
 config/locales/tr.yml                              |  13 -
 config/locales/uk.yml                              |  13 -
 config/locales/zh-CN.yml                           |  13 -
 config/locales/zh-HK.yml                           |  13 -
 config/locales/zh-TW.yml                           |  13 -
 config/settings.yml                                |   1 +
 lib/tasks/mastodon.rake                            |   8 +-
 spec/requests/localization_spec.rb                 |   8 +-
 spec/views/about/show.html.haml_spec.rb            |   9 +-
 68 files changed, 956 insertions(+), 655 deletions(-)
 create mode 100644 app/javascript/fonts/montserrat/Montserrat-Medium.ttf
 create mode 100644 app/javascript/images/cloud2.png
 create mode 100644 app/javascript/images/cloud3.png
 create mode 100644 app/javascript/images/cloud4.png
 create mode 100644 app/javascript/images/elephant-fren.png
 create mode 100644 app/javascript/mastodon/containers/timeline_container.js
 create mode 100644 app/javascript/mastodon/features/standalone/public_timeline/index.js
 create mode 100644 app/views/about/_features.html.haml

(limited to 'spec')

diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index c0addbecc..47690e81e 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -4,7 +4,10 @@ class AboutController < ApplicationController
   before_action :set_body_classes
   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/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index f27a1f4d4..29b590d7a 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -11,8 +11,15 @@ module Admin
       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
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 218da6906..8a8b9ec76 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -15,12 +15,16 @@ class HomeController < ApplicationController
   end
 
   def set_initial_state_json
-    state = InitialStatePresenter.new(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))
-
-    serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer)
+    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/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf
new file mode 100644
index 000000000..88d70b89c
Binary files /dev/null and b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf differ
diff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png
new file mode 100644
index 000000000..f325ca6de
Binary files /dev/null and b/app/javascript/images/cloud2.png differ
diff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png
new file mode 100644
index 000000000..ab194d0b8
Binary files /dev/null and b/app/javascript/images/cloud3.png differ
diff --git a/app/javascript/images/cloud4.png b/app/javascript/images/cloud4.png
new file mode 100644
index 000000000..98323f5a2
Binary files /dev/null and b/app/javascript/images/cloud4.png differ
diff --git a/app/javascript/images/elephant-fren.png b/app/javascript/images/elephant-fren.png
new file mode 100644
index 000000000..3b64edf08
Binary files /dev/null and b/app/javascript/images/elephant-fren.png differ
diff --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/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/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 75222e965..89a358e38 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -11,18 +11,44 @@ const messages = defineMessages({
 
 class Item extends React.PureComponent {
 
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
   static propTypes = {
     attachment: ImmutablePropTypes.map.isRequired,
     index: PropTypes.number.isRequired,
     size: PropTypes.number.isRequired,
     onClick: PropTypes.func.isRequired,
-    autoPlayGif: PropTypes.bool.isRequired,
+    autoPlayGif: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    autoPlayGif: false,
   };
 
+  handleMouseEnter = (e) => {
+    if (this.hoverToPlay()) {
+      e.target.play();
+    }
+  }
+
+  handleMouseLeave = (e) => {
+    if (this.hoverToPlay()) {
+      e.target.pause();
+      e.target.currentTime = 0;
+    }
+  }
+
+  hoverToPlay () {
+    const { attachment, autoPlayGif } = this.props;
+    return !autoPlayGif && attachment.get('type') === 'gifv';
+  }
+
   handleClick = (e) => {
     const { index, onClick } = this.props;
 
-    if (e.button === 0) {
+    if (this.context.router && e.button === 0) {
       e.preventDefault();
       onClick(index);
     }
@@ -116,6 +142,8 @@ class Item extends React.PureComponent {
             role='application'
             src={attachment.get('url')}
             onClick={this.handleClick}
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
             autoPlay={autoPlay}
             loop
             muted
@@ -144,7 +172,11 @@ export default class MediaGallery extends React.PureComponent {
     height: PropTypes.number.isRequired,
     onOpenMedia: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
-    autoPlayGif: PropTypes.bool.isRequired,
+    autoPlayGif: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    autoPlayGif: false,
   };
 
   state = {
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/components/status.js b/app/javascript/mastodon/components/status.js
index df771f5a8..6b9fdd2af 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -140,12 +140,16 @@ export default class Status extends ImmutablePureComponent {
   }
 
   handleClick = () => {
+    if (!this.context.router) {
+      return;
+    }
+
     const { status } = this.props;
     this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
   }
 
   handleAccountClick = (e) => {
-    if (e.button === 0) {
+    if (this.context.router && e.button === 0) {
       const id = Number(e.currentTarget.getAttribute('data-id'));
       e.preventDefault();
       this.context.router.history.push(`/accounts/${id}`);
@@ -236,7 +240,7 @@ export default class Status extends ImmutablePureComponent {
         <div className='status__info'>
           <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
 
-          <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
+          <a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
             <div className='status__avatar'>
               {statusAvatar}
             </div>
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index fd7c99054..7bb394e71 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -40,7 +40,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onBlock: PropTypes.func,
     onReport: PropTypes.func,
     onMuteConversation: PropTypes.func,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.number,
     withDismiss: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -97,6 +97,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     const { status, me, intl, withDismiss } = this.props;
     const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
     const mutingConversation = status.get('muted');
+    const anonymousAccess = !me;
 
     let menu = [];
     let reblogIcon = 'retweet';
@@ -137,12 +138,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
 
     return (
       <div className='status__action-bar'>
-        <IconButton className='status__action-bar-button' title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
-        <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
-        <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
+        <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
+        <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
+        <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
 
         <div className='status__action-bar-dropdown'>
-          <DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
+          <DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 02b4c8402..1b803a22e 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -6,6 +6,7 @@ import emojify from '../emoji';
 import { isRtl } from '../rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
+import classnames from 'classnames';
 
 export default class StatusContent extends React.PureComponent {
 
@@ -43,10 +44,11 @@ export default class StatusContent extends React.PureComponent {
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
       } else {
-        link.setAttribute('target', '_blank');
-        link.setAttribute('rel', 'noopener');
         link.setAttribute('title', link.href);
       }
+
+      link.setAttribute('target', '_blank');
+      link.setAttribute('rel', 'noopener');
     }
   }
 
@@ -59,7 +61,7 @@ export default class StatusContent extends React.PureComponent {
   }
 
   onMentionClick = (mention, e) => {
-    if (e.button === 0) {
+    if (this.context.router && e.button === 0) {
       e.preventDefault();
       this.context.router.history.push(`/accounts/${mention.get('id')}`);
     }
@@ -68,7 +70,7 @@ export default class StatusContent extends React.PureComponent {
   onHashtagClick = (hashtag, e) => {
     hashtag = hashtag.replace(/^#/, '').toLowerCase();
 
-    if (e.button === 0) {
+    if (this.context.router && e.button === 0) {
       e.preventDefault();
       this.context.router.history.push(`/timelines/tag/${hashtag}`);
     }
@@ -120,6 +122,9 @@ export default class StatusContent extends React.PureComponent {
     const content = { __html: emojify(status.get('content')) };
     const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
     const directionStyle = { direction: 'ltr' };
+    const classNames = classnames('status__content', {
+      'status__content--with-action': this.props.onClick && this.context.router,
+    });
 
     if (isRtl(status.get('search_index'))) {
       directionStyle.direction = 'rtl';
@@ -141,7 +146,7 @@ export default class StatusContent extends React.PureComponent {
       }
 
       return (
-        <div className='status__content status__content--with-action' ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
           <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
             <span dangerouslySetInnerHTML={spoilerContent} />
             {' '}
@@ -157,7 +162,7 @@ export default class StatusContent extends React.PureComponent {
       return (
         <div
           ref={this.setRef}
-          className='status__content status__content--with-action'
+          className={classNames}
           style={directionStyle}
           onMouseDown={this.handleMouseDown}
           onMouseUp={this.handleMouseUp}
diff --git a/app/javascript/mastodon/components/video_player.js b/app/javascript/mastodon/components/video_player.js
index 452a84319..999cf42d9 100644
--- a/app/javascript/mastodon/components/video_player.js
+++ b/app/javascript/mastodon/components/video_player.js
@@ -14,6 +14,10 @@ const messages = defineMessages({
 @injectIntl
 export default class VideoPlayer extends React.PureComponent {
 
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     width: PropTypes.number,
@@ -119,11 +123,15 @@ export default class VideoPlayer extends React.PureComponent {
       </div>
     );
 
-    let expandButton = (
-      <div className='status__video-player-expand'>
-        <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
-      </div>
-    );
+    let expandButton = '';
+
+    if (this.context.router) {
+      expandButton = (
+        <div className='status__video-player-expand'>
+          <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
+        </div>
+      );
+    }
 
     let muteButton = '';
 
@@ -138,7 +146,7 @@ export default class VideoPlayer extends React.PureComponent {
     if (!this.state.visible) {
       if (sensitive) {
         return (
-          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
+          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
             {spoilerButton}
             <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
             <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@@ -146,7 +154,7 @@ export default class VideoPlayer extends React.PureComponent {
         );
       } else {
         return (
-          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
+          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
             {spoilerButton}
             <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
             <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
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/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/packs/public.js b/app/javascript/packs/public.js
index 254250a3b..0b00da39d 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -4,6 +4,9 @@ import { delegate } from 'rails-ujs';
 import emojify from '../mastodon/emoji';
 import { getLocale } from '../mastodon/locales';
 import loadPolyfills from '../mastodon/load_polyfills';
+import TimelineContainer from '../mastodon/containers/timeline_container';
+import React from 'react';
+import ReactDOM from 'react-dom';
 
 require.context('../images/', true);
 
@@ -36,6 +39,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() {
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss
index 3512bdcb4..b9c018391 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/about.scss
@@ -116,10 +116,6 @@
     .wrapper {
       padding: 20px;
     }
-
-    .features-list {
-      display: block;
-    }
   }
 }
 
@@ -301,80 +297,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 4da698e81..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;
@@ -22,6 +22,11 @@ body {
     background: $ui-base-color;
   }
 
+  &.about-body {
+    background: darken($ui-base-color, 8%);
+    padding-bottom: 0;
+  }
+
   &.embed {
     background: transparent;
     margin: 0;
diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss
index 8d6478e10..5eb3149ef 100644
--- a/app/javascript/styles/boost.scss
+++ b/app/javascript/styles/boost.scss
@@ -12,3 +12,7 @@ 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='#{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>");
   }
 }
+
+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 def69d250..8d0350eb6 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 {
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 414dc4fe8..e723b50ff 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 {
@@ -197,8 +211,6 @@ code {
 
     &:active,
     &:focus {
-      position: relative;
-      top: 1px;
       background-color: darken($ui-highlight-color, 5%);
     }
 
@@ -219,6 +231,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 +273,7 @@ code {
   text-align: center;
 
   a {
-    color: $primary-text-color;
+    color: $ui-primary-color;
     text-decoration: none;
 
     &:hover {
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index 63ef23d5d..5d5be58ba 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -5,6 +5,7 @@ class InstancePresenter
     :closed_registrations_message,
     :site_contact_email,
     :open_registrations,
+    :site_title,
     :site_description,
     :site_extended_description,
     :site_terms,
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 49ff9e377..6751c9411 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -5,32 +5,41 @@ class InitialStateSerializer < ActiveModel::Serializer
              :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,
-      me: object.current_account.id,
       admin: object.admin&.id,
-      boost_modal: object.current_account.user.setting_boost_modal,
-      delete_modal: object.current_account.user.setting_delete_modal,
-      auto_play_gif: object.current_account.user.setting_auto_play_gif,
-      system_font_ui: object.current_account.user.setting_system_font_ui,
     }
+
+    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
-    {
-      me: object.current_account.id,
-      default_privacy: object.current_account.user.setting_default_privacy,
-      default_sensitive: object.current_account.user.setting_default_sensitive,
-    }
+    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)
-    store[object.admin.id]           = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) unless object.admin.nil?
+    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
 
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 d15b04163..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://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
-          ·
-          = 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/tootsuite/mastodon'
-      ·
-      = link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
+      .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/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 3096a958d..59192530b 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -12,54 +12,53 @@
       %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')
+        %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')
+        %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
+        %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
+        %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
+        %td= text_area_tag :site_extended_description, @settings['site_extended_description'].value, rows: 8
       %tr
         %td
           %strong= t('admin.settings.site_terms.title')
           %p= t('admin.settings.site_terms.desc_html')
-        %td= text_area_tag :site_terms,
-          @settings['site_terms'].value,
-          rows: 8
+        %td= text_area_tag :site_terms, @settings['site_terms'].value, rows: 8
       %tr
         %td
           %strong= t('admin.settings.registrations.open.title')
+          %p= t('admin.settings.registrations.open.desc_html')
         %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)
+          = select_tag :open_registrations, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => 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
+        %td= text_area_tag :closed_registrations_message, @settings['closed_registrations_message'].value, rows: 8
+      %tr
+        %td
+          %strong= t('admin.settings.registrations.deletion.title')
+          %p= t('admin.settings.registrations.deletion.desc_html')
+        %td
+          = select_tag :open_deletion, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['open_deletion'].value)
+      %tr
+        %td
+          %strong= t('admin.settings.timeline_preview.title')
+          %p= t('admin.settings.timeline_preview.desc_html')
+        %td
+          = select_tag :timeline_preview, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['timeline_preview'].value)
 
   .simple_form.actions
     = button_tag t('generic.save_changes'), type: :submit, class: :btn
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/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/config/locales/ar.yml b/config/locales/ar.yml
index 4d045dee1..d395dc9c3 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -3,23 +3,12 @@ ar:
   about:
     about_mastodon: ماستدون شبكة إجتماعية <em>حرة و مفتوحة المصدر</em>. هو بديل <em>لامركزي</em> لمنصات تجارية ، يمكنك من تجنب احتكار شركة واحدة للإتصالات الخاصة بك. يمكنك اختيار أي خادم تثق فيه. أيهما تختار، يمكنك التفاعل مع أي شخص آخر على الشبكة. يمكن لأي شخص تنصيب و تشغيل خادم ماستدون خاص به والمشاركة في <em>الشبكات الاجتماعية</em> بكل شفافية.
     about_this: عن مثيل الخادوم هذا
-    apps: التطبيقات
     business_email: 'البريد الإلكتروني المهني :'
     closed_registrations: التسجيلات في مثيل الخادوم هذا مُغلقة حاليًا.
     contact: للتواصل معنا
     description_headline: ما هو %{domain}?
     domain_count_after: خوادم أخرى
     domain_count_before: متصل بـ
-    features:
-      api: واجهة برمجة مفتوحة للتطبيقات والخدمات
-      blocks: نص منسق وأدوات كتم
-      characters: 500 حرف في كل رسالة
-      chronology: خيوط متسلسلة زمنيا
-      ethics: 'تصميم أخلاقي : لا إعلانات و لا تعقُّب'
-      gifv: مجموعات صور GIFV وأشرطة فيديو قصيرة
-      privacy: إعدادات مدققة لخصوصية كل منشور
-      public: الخيوط الزمنية العمومية
-    features_headline: ما الذي يجعل ماستدون فريدًا ؟
     get_started: إبدأ الآن
     links: الروابط
     other_instances: خوادم أخرى
@@ -93,7 +82,7 @@ ar:
       blocking: قائمة المحظورين
       following: قائمة المستخدمين المتبوعين
     upload: تحميل
-  landing_strip_html: <strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse..
+  landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse.."
   landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
   media_attachments:
     validations:
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 76d6a2605..042d609b0 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -3,23 +3,12 @@ bg:
   about:
     about_mastodon: Mastodon е <em>безплатен</em> сървър с <em>отворен код</em> за социални мрежи. Като <em>децентрализирана</em> алтернатива на комерсиалните платформи, той позволява избягването на риска от монополизация на твоята комуникация от единични компании. Изберете си сървър, на който се доверявате, и ще можете да контактувате с всички останали. Всеки може да пусне Mastodon и лесно да вземе участие в <em>социалната мрежа</em>.
     about_this: За тази инстанция
-    apps: Приложения
     business_email: 'Служебен e-mail:'
     closed_registrations: В момента регистрациите за тази инстанция са затворени.
     contact: За контакти
     description_headline: Какво е %{domain}?
     domain_count_after: други инстанции
     domain_count_before: Свързани към
-    features:
-      api: Отворено API за приложения и услуги
-      blocks: Богат на инструменти за блокиране и заглушаване
-      characters: Публикации от 500 символа
-      chronology: Публикациите се показват хронологично
-      ethics: 'Етичен дизайн: без реклами и проследяване'
-      gifv: GIFV комплекти и кратки видео клипове
-      privacy: Настройване на поверителността за всяка публикация
-      public: Публични канали
-    features_headline: Какво откроява Mastodon
     get_started: Първи стъпки
     links: Връзки
     other_instances: Други инстанции
@@ -93,7 +82,7 @@ bg:
       blocking: Списък на блокираните
       following: Списък на последователите
     upload: Качване
-  landing_strip_html: <strong>%{name}</strong> е потребител от %{link_to_root_path}. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon.
+  landing_strip_html: "<strong>%{name}</strong> е потребител от %{link_to_root_path}. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon."
   landing_strip_signup_html: Ако нямаш акаунт, можеш да си <a href="%{sign_up_path}">създадеш ето тук</a>.
   media_attachments:
     validations:
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 2fbc63ef9..10c34498b 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -3,23 +3,12 @@ ca:
   about:
     about_mastodon: Mastodon és un servidor de xarxa social <em>lliure i de codi obert</em>. Una alternativa <em>descentralitzada</em> a plataformes comercials, que evita el risc que una única companyia monopolitzi la teva comunicació. Qualsevol pot executar Mastodon i participar sense problemes en la <em>xarxa social</em>.
     about_this: Sobre aquesta instància
-    apps: Apps
     business_email: 'Adreça de contacte:'
     closed_registrations: Els registres estan actualment tancats en aquesta instància.
     contact: Contacte
     description_headline: Què es %{domain}?
     domain_count_after: altres instàncies
     domain_count_before: Connectat a
-    features:
-      api: API pública per a aplicacions i serveis
-      blocks: Moderació de contingut
-      characters: 500 caràcters per publicació
-      chronology: Les histories son cronològiques
-      ethics: 'Disseny ètic: sense anuncis, sense rastrejos'
-      gifv: Vídeos curts i GIFV
-      privacy: Configuracions de privacitat ajustables
-      public: Història federada
-    features_headline: El que distingeix a Mastodon
     get_started: Començar
     links: Vincles
     other_instances: Altres instàncies
@@ -173,8 +162,6 @@ ca:
           desc_html: Apareix en la primera pàgina quan es tanquen els registres<br>Pot utilitzar etiquetes HTML
           title: Missatge de registre tancat
         open:
-          disabled: Desactivat
-          enabled: Activat
           title: Registre obert
       setting: Ajust
       site_description:
@@ -203,8 +190,8 @@ ca:
     change_password: Canviar contrasenya
     delete_account: Esborrar el compte
     delete_account_html: Si vols esborrar el teu compte pots <a href="%{path}">fer-ho aquí</a>. S'et demanarà confirmació.
-    didnt_get_confirmation: "No vas rebre el correu de confirmació?"
-    forgot_password: "Has oblidat la contrasenya?"
+    didnt_get_confirmation: No vas rebre el correu de confirmació?
+    forgot_password: Has oblidat la contrasenya?
     login: Iniciar sessió
     logout: Tancar sessió
     register: Enregistrarse
diff --git a/config/locales/de.yml b/config/locales/de.yml
index f2841d0b7..2bdb87708 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -3,23 +3,12 @@ de:
   about:
     about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> sozialer Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen.
     about_this: Über diese Instanz
-    apps: Apps
     business_email: 'Geschäftliche E-Mail:'
     closed_registrations: Die Registrierung ist auf dieser Instanz momentan geschlossen.
     contact: Kontakt
     description_headline: Was ist %{domain}?
     domain_count_after: andere Instanzen
     domain_count_before: Verbunden mit
-    features:
-      api: Offene API für Apps und Dienste
-      blocks: Mächtige Block- und Stummschaltungswerkzeuge
-      characters: 500 Zeichen pro Beitrag
-      chronology: Zeitleisten sind chronologisch
-      ethics: 'Ethisches Design: keine Werbung, kein Tracking'
-      gifv: GIFV-Sets und kurze Videos
-      privacy: Granulare Privatsphäre-Einstellungen für jeden Beitrag
-      public: Öffentliche Zeitleisten
-    features_headline: Was Mastodon einzigartig macht
     get_started: Erste Schritte
     links: Links
     other_instances: Andere Instanzen
@@ -147,8 +136,6 @@ de:
           desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen
           title: Nachricht über geschlossene Registrierung
         open:
-          disabled: Deaktiviert
-          enabled: Aktiviert
           title: Offene Registrierung
       setting: Einstellung
       site_description:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8bb893d1c..e33dde038 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,28 +1,30 @@
 ---
 en:
   about:
-    about_mastodon: Mastodon is a <em>free, open-source</em> social network. A <em>decentralized</em> alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Pick a server that you trust &mdash; whichever you choose, you can interact with everyone else. Anyone can run their own Mastodon instance and participate in the <em>social network</em> seamlessly.
-    about_this: About this instance
-    apps: Apps
+    about_mastodon_html: Mastodon is a social network based on open web protocols and free, open-source software. It is decentralized like e-mail.
+    about_this: About
     business_email: 'Business e-mail:'
-    closed_registrations: Registrations are currently closed on this instance.
+    closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there.
     contact: Contact
     description_headline: What is %{domain}?
     domain_count_after: other instances
     domain_count_before: Connected to
     features:
-      api: Open API for apps and services
-      blocks: Rich block and muting tools
-      characters: 500 characters per post
-      chronology: Timelines are chronological
-      ethics: 'Ethical design: no ads, no tracking'
-      gifv: GIFV sets and short videos
-      privacy: Granular, per-post privacy settings
-      public: Public timelines
-    features_headline: What sets Mastodon apart
+      humane_approach_body: Learning from failures of other networks, Mastodon aims to make ethical design choices to combat the misuse of social media.
+      humane_approach_title: A more humane approach
+      not_a_product_body: Mastodon is not a commercial network. No advertising, no data mining, no walled gardens. There is no central authority.
+      not_a_product_title: You’re a person, not a product
+      real_conversation_body: With 500 characters at your disposal and support for granular content and media warnings, you can express yourself the way you want to.
+      real_conversation_title: Built for real conversation
+      within_reach_body: Multiple apps for iOS, Android, and other platforms thanks to a developer-friendly API ecosystem allow you to keep up with your friends anywhere.
+      within_reach_title: Always within reach
+    find_another_instance: Find another instance
+    generic_description: "%{domain} is one server in the network"
     get_started: Get started
+    hosted_on: Mastodon hosted on %{domain}
+    learn_more: Learn more
     links: Links
-    other_instances: Other instances
+    other_instances: Instance list
     source_code: Source code
     status_count_after: statuses
     status_count_before: Who authored
@@ -30,6 +32,7 @@ en:
     user_count_after: users
     user_count_before: Home to
     version: Version
+    what_is_mastodon: What is Mastodon?
   accounts:
     follow: Follow
     followers: Followers
@@ -173,9 +176,14 @@ en:
           desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags
           title: Closed registration message
         open:
-          disabled: Disabled
-          enabled: Enabled
+          desc_html: Allow anyone to create an account
           title: Open registration
+        deletion:
+          desc_html: Allow anyone to delete their account
+          title: Open deletion
+      timeline_preview:
+        desc_html: Display public timeline on landing page
+        title: Timeline preview
       setting: Setting
       site_description:
         desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
@@ -185,7 +193,7 @@ en:
         title: Extended site description
       site_terms:
         desc_html: Displayed on terms page<br>You can use HTML tags
-        title: Site Privacy Policy
+        title: Privacy policy
       site_title: Site title
       title: Site Settings
     subscriptions:
@@ -207,6 +215,7 @@ en:
   applications:
     invalid_url: The provided URL is invalid
   auth:
+    agreement_html: By signing up you agree to <a href="%{rules_path}">our terms of service</a> and <a href="%{terms_path}">privacy policy</a>.
     change_password: Security
     delete_account: Delete account
     delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 8b42415df..d47a5db0e 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -3,22 +3,11 @@ eo:
   about:
     about_mastodon: Mastodon estas <em>senpaga, malfermitkoda</em> socia reto. Ĝi estas <em>sencentra</em> alia eblo al komercaj servoj. Ĝi evitigas, ke unusola firmao regu vian tutan komunikadon. Elektu servilon, kiun vi fidas. Kiu ajn estas via elekto, vi povas interagi kun ĉiuj aliaj uzantoj. Iu ajn povas krei sian propran aperaĵon de Mastodon en sia servilo, kaj partopreni en la <em>socia reto</em> tute glate.
     about_this: Pri tiu aperaĵo
-    apps: Aplikaĵoj
     business_email: 'Profesia retpoŝt-adreso:'
     contact: Kontakti
     description_headline: Kio estas %{domain}?
     domain_count_after: aliaj aperaĵoj
     domain_count_before: Konektita al
-    features:
-      api: Malfermita API por aplikaĵoj kaj servoj
-      blocks: Kompletaj iloj por bloki kaj kaŝi
-      characters: Po 500 signoj por ĉiu mesaĝo
-      chronology: Tempolinioj laŭtempaj
-      ethics: 'Etike kreita: neniu reklamo, neniu ŝpurado'
-      gifv: Eblo diskonigi etajn videojn kaj GIFV
-      privacy: Videbleco agordita laŭ la mesaĝo
-      public: Publikaj tempolinioj
-    features_headline: Kiel Mastodon estas malsimila
     get_started: Komenci
     links: Ligiloj
     other_instances: Aliaj aperaĵoj
@@ -92,7 +81,7 @@ eo:
       blocking: Listo de blokitoj
       following: Listo de sekvatoj
     upload: Alporti
-  landing_strip_html: <strong>%{name}</strong> estas uzanto en %{link_to_root_path}. Vi povas sekvi tiun aŭ interagi kun tiu, se vi havas konton ie ajn en la Fediverse.
+  landing_strip_html: "<strong>%{name}</strong> estas uzanto en %{link_to_root_path}. Vi povas sekvi tiun aŭ interagi kun tiu, se vi havas konton ie ajn en la Fediverse."
   landing_strip_signup_html: Se vi ne havas, vi povas <a href="%{sign_up_path}">membriĝi ĉi tie.</a>.
   notification_mailer:
     digest:
@@ -105,19 +94,19 @@ eo:
         one: "1 nova sciigo ekde via lasta vizito \U0001F418"
         other: "%{count} novaj sciigoj ekde via lasta vizito \U0001F418"
     favourite:
-      body: '%{name} favoris vian mesaĝon:'
+      body: "%{name} favoris vian mesaĝon:"
       subject: "%{name} favoris vian mesaĝon"
     follow:
       body: "%{name} eksekvis vin:"
       subject: "%{name} eksekvis vin"
     follow_request:
       body: "%{name} petis sekvi vin:"
-      subject: '%{name} petis sekvi vin'
+      subject: "%{name} petis sekvi vin"
     mention:
-      body: '%{name} menciis vin en:'
-      subject: '%{name} menciis vin'
+      body: "%{name} menciis vin en:"
+      subject: "%{name} menciis vin"
     reblog:
-      body: '%{name} diskonigis vian mesaĝon:'
+      body: "%{name} diskonigis vian mesaĝon:"
       subject: "%{name} diskonigis vian mesaĝon"
   pagination:
     next: Sekva
diff --git a/config/locales/es.yml b/config/locales/es.yml
index f587bb4ec..c051c9a08 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -3,23 +3,12 @@ es:
   about:
     about_mastodon: Mastodon es un servidor de red social <em>libre y de código abierto</em>. Una alternativa <em>descentralizada</em> a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la <em>red social</em>.
     about_this: Acerca de esta instancia
-    apps: Apps
     business_email: 'Correo de negocios:'
     closed_registrations: Los registros están actualmente cerrados en esta instancia.
     contact: Contacto
     description_headline: "¿Qué es %{domain}?"
     domain_count_after: otras instancias
     domain_count_before: Conectado a
-    features:
-      api: API pública para aplicaciones y servicios
-      blocks: Moderación de contenido
-      characters: 500 caracteres por publicación
-      chronology: Las historias son cronológicas
-      ethics: 'Diseño etico: sin anuncios, sin rastreos'
-      gifv: Videos cortos y GIFV
-      privacy: Configuraciones de privacidad ajustables
-      public: Historia federada
-    features_headline: Lo que distingue a Mastodon
     get_started: Comenzar
     links: Enlaces
     other_instances: Otras instancias
@@ -93,7 +82,7 @@ es:
       blocking: Lista de bloqueados
       following: Lista de seguidos
     upload: Cargar
-  landing_strip_html: <strong>%{name}</strong> es un usuario en %{link_to_root_path}. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse.
+  landing_strip_html: "<strong>%{name}</strong> es un usuario en %{link_to_root_path}. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse."
   landing_strip_signup_html: Si no tienes una, puedes <a href="%{sign_up_path}">registrar aquí</a>.
   media_attachments:
     validations:
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 515443608..6f0bd0839 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -3,23 +3,12 @@ fa:
   about:
     about_mastodon: ماستدون (Mastodon) یک شبکهٔ اجتماعی <em>آزاد و کدباز</em> است. یک جایگزین <em>غیرمتمرکز</em> برای شبکه‌های تجاری، که نمی‌گذارد ارتباط‌های شما را یک شرکت در انحصار خود بگیرد. یک سرور مورد اعتماد را انتخاب کنید &mdash; هر سروری که باشد، همچنان می‌توانید با سرورهای دیگر ارتباط داشته باشید. هر کسی می‌تواند سرور ماستدون خود را راه بیندازد و در <em>شبکهٔ اجتماعی</em> سهیم شود.
     about_this: دربارهٔ این سرور
-    apps: برنامه‌ها
     business_email: 'ایمیل کاری:'
     closed_registrations: امکان ثبت نام روی این سرور هم‌اینک فعال نیست.
     contact: تماس
     description_headline: "%{domain} چیست؟"
     domain_count_after: سرور دیگر
     domain_count_before: متصل به
-    features:
-      api: رابط برنامه‌نویسی برای برنامه‌ها و سرویس‌های دیگر
-      blocks: ابزارهای قدرتمند برای مسدود یا بی‌صدا کردن دیگران
-      characters: ۵۰۰ حرف برای هر نوشته
-      chronology: نمایش نوشته‌های دیگران به ترتیب زمانی
-      ethics: 'اخلاق‌مدار: بدون تبلیغات، بدون ردگیری'
-      gifv: تصاویر متحرک و ویدیوهای کوتاه
-      privacy: تنظیمات حریم خصوصی جداگانه برای هر نوشته
-      public: نمایش نوشته‌های عمومی دیگران از همه‌جا
-    features_headline: برگ‌های برندهٔ ماستدون
     get_started: آغاز کنید
     links: پیوندها
     other_instances: سرورهای دیگر
@@ -159,8 +148,6 @@ fa:
           desc_html: وقتی امکان ثبت نام روی سرور فعال نباشد در صفحهٔ اصلی نمایش می‌یابد<br>می‌توانید HTML بنویسید
           title: پیغام برای فعال‌نبودن ثبت نام
         open:
-          disabled: غیرفعال
-          enabled: فعال
           title: امکان ثبت نام
       setting: تنظیمات
       site_description:
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index c20bf690b..a2488530f 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -3,22 +3,11 @@ fi:
   about:
     about_mastodon: Mastodon on <em>ilmainen, avoimeen lähdekoodiin perustuva</em> sosiaalinen verkosto. <em>Hajautettu</em> vaihtoehto kaupallisille alustoille, se välttää eiskit yhden yrityksen monopolisoinnin sinun viestinnässäsi. Valitse palvelin mihin luotat &mdash; minkä tahansa valitset, voit vuorovaikuttaa muiden kanssa. Kuka tahansa voi luoda Mastodon palvelimen ja ottaa osaa <em>sosiaaliseen verkkoon</em> saumattomasti.
     about_this: Tietoja tästä palvelimesta
-    apps: Ohjelmat
     business_email: 'Business e-mail:'
     contact: Ota yhteyttä
     description_headline: Mikä on %{domain}?
     domain_count_after: muuhun palvelimeen
     domain_count_before: Yhdistyneenä
-    features:
-      api: Avoin API ohjelmille ja palveluille
-      blocks: Rikkaat esto- ja hiljennystyökalut
-      characters: 500 kirjainta per viesti
-      chronology: Aikajana on kronologisessa järjestyksessä
-      ethics: 'Eettinen suunnittelu: ei mainoksia, ei seurantaa'
-      gifv: GIFV settejä ja lyhyitä videoita
-      privacy: Julkaisukohtainen yksityisyysasetus
-      public: Julkiset aikajanat
-    features_headline: Mikä erottaa Mastodonin muista
     get_started: Aloita käyttö
     links: Linkit
     other_instances: Muut palvelimet
@@ -92,7 +81,7 @@ fi:
       blocking: Estetyt lista
       following: Seuratut lista
     upload: Lähetä
-  landing_strip_html: <strong>%{name}</strong> on käyttäjä domainilla %{link_to_root_path}. Voit seurata tai vuorovaikuttaa heidän kanssaan jos sinulla on tili yleisessä verkossa.
+  landing_strip_html: "<strong>%{name}</strong> on käyttäjä domainilla %{link_to_root_path}. Voit seurata tai vuorovaikuttaa heidän kanssaan jos sinulla on tili yleisessä verkossa."
   landing_strip_signup_html: Jos sinulla ei ole tiliä, voit <a href="%{sign_up_path}">rekisteröityä täällä</a>.
   notification_mailer:
     digest:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index fcf5f6f9e..9eeafaa6e 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -3,23 +3,12 @@ fr:
   about:
     about_mastodon: Mastodon est un serveur <em>libre</em> de réseautage social. Alternative <em>décentralisée</em> aux plateformes commerciales, la monopolisation de vos communications par une entreprise unique est évitée. Tout un chacun peut faire tourner Mastodon et participer au <em>réseau social</em> de manière transparente.
     about_this: À propos de cette instance
-    apps: Applications
     business_email: Courriel professionnel
     closed_registrations: Les inscriptions sont actuellement fermées sur cette instance.
     contact: Contact
     description_headline: Qu’est-ce que %{domain} ?
     domain_count_after: autres instances
     domain_count_before: Connectés à
-    features:
-      api: API ouverte aux apps et services
-      blocks: Outils complets de bloquage et masquage
-      characters: 500 caractères par post
-      chronology: Fil chronologique
-      ethics: Pas de pubs, pas de pistage
-      gifv: Partage de vidéos et de GIFs
-      privacy: Réglages de confidentialité au niveau des posts
-      public: Fils publics
-    features_headline: Ce qui rend Mastodon différent
     get_started: Rejoindre le réseau
     links: Liens
     other_instances: Autres instances
@@ -48,7 +37,7 @@ fr:
       create:
         name: "%{account_name} a créé une note."
     outbox:
-      name: "Boîte d’envoi de %{account_name}"
+      name: Boîte d’envoi de %{account_name}
       summary: Liste d’activités de %{account_name}
   admin:
     accounts:
@@ -173,8 +162,6 @@ fr:
           desc_html: Affiché sur la page d’accueil lorsque les inscriptions sont fermées<br>Vous pouvez utiliser des balises HTML
           title: Message de fermeture des inscriptions
         open:
-          disabled: Désactivées
-          enabled: Activées
           title: Inscriptions
       setting: Paramètre
       site_description:
diff --git a/config/locales/he.yml b/config/locales/he.yml
index ec7d972ec..760ddac00 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -3,23 +3,12 @@ he:
   about:
     about_mastodon: מסטודון היא רשת חברתית <em>חופשית, מבוססת תוכנה חופשית ("קוד פתוח")</em>. כאלטרנטיבה <em>בלתי ריכוזית</em> לפלטפרומות המסחריות, מסטודון מאפשרת להמנע מהסיכונים הנלווים להפקדת התקשורת שלך בידי חברה יחידה. שמת את מבטחך בשרת אחד &mdash; לא משנה במי בחרת, תמיד אפשר לדבר עם כל שאר המשתמשים. לכל מי שרוצה יש את האפשרות להקים שרת מסטודון עצמאי, ולהשתתף ב<em>רשת החברתית</em> באופן חלק.
     about_this: אודות שרת זה
-    apps: ישומים
     business_email: 'דוא"ל עסקי:'
     closed_registrations: הרשמות סגורות לשרת זה לעת עתה.
     contact: צור קשר
     description_headline: מהו %{domain}?
     domain_count_after: שרתים אחרים
     domain_count_before: מחובר אל
-    features:
-      api: API פתוח לישומים ושירותים
-      blocks: כלי חסימה והשתקה חזקים
-      characters: 500 תווים להודעה
-      chronology: הטורים כרונולוגיים
-      ethics: 'תכנון מוסרי: אין פרסומות, אין מעקב'
-      gifv: GIFV וסרטונים קצרים
-      privacy: אפשרויות פרטיוּת נפרדות לכל הודעה
-      public: טורים פומביים
-    features_headline: מה מייחד קהילות מבוססות מסטודון
     get_started: בואו נתחיל
     links: קישורים
     other_instances: שרתים אחרים
@@ -167,8 +156,6 @@ he:
           desc_html: מוצג על הדף הראשי כאשר ההרשמות סגורות<br>ניתן להשתמש בתגיות HTML
           title: מסר סגירת הרשמות
         open:
-          disabled: מבוטל
-          enabled: מופעל
           title: הרשמה פתוחה
       setting: הגדרה
       site_description:
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index f873fc7ed..8297ca629 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -3,23 +3,12 @@ hr:
   about:
     about_mastodon: Mastodon je <em>besplatna, open-source</em> socijalna mreža. <em>Decentralizirana</em> alternativa komercijalnim platformama, izbjegava rizik toga da jedna tvrtka monopolizira vašu komunikaciju. Izaberite server kojem ćete vjerovati &mdash; koji god odabrali, moći ćete komunicirati sa svima ostalima. Bilo tko može imati svoju vlastitu Mastodon instancu i sudjelovati u <em>socijalnoj mreži</em> bez problema.
     about_this: O ovoj instanci
-    apps: Aplikacije
     business_email: 'Poslovni e-mail:'
     closed_registrations: Registracije na ovoj instanci su trenutno zatvorene.
     contact: Kontakt
     description_headline: Što je %{domain}?
     domain_count_after: druge instance
     domain_count_before: Spojen na
-    features:
-      api: Otvoren API za aplikacije i servise
-      blocks: Bogati alati za blokiranje i ušutkivanje
-      characters: 500 znakova po postu
-      chronology: Timelines su kronološke
-      ethics: 'Etički dizajn: bez oglasa, bez praćenja'
-      gifv: GIFV setovi i kratki videi
-      privacy: Granularne postavke privatnosti, po postu
-      public: Javne timelines
-    features_headline: Po čemu se Mastodon razlikuje
     get_started: Započni
     links: Linkovi
     other_instances: Druge instance
@@ -94,7 +83,7 @@ hr:
       following: Lista onih koje slijedim
       muting: Lista utišanih
     upload: Upload
-  landing_strip_html: <strong>%{name}</strong> je korisnik na %{link_to_root_path}. Možeš ih slijediti ili komunicirati s njima ako imaš račun igdje u fediversu.
+  landing_strip_html: "<strong>%{name}</strong> je korisnik na %{link_to_root_path}. Možeš ih slijediti ili komunicirati s njima ako imaš račun igdje u fediversu."
   landing_strip_signup_html: Ako nemaš, možeš se <a href="%{sign_up_path}">registrirati ovdje</a>.
   notification_mailer:
     digest:
diff --git a/config/locales/id.yml b/config/locales/id.yml
index fc4ffd046..4bcd8f2ed 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -3,23 +3,12 @@ id:
   about:
     about_mastodon: Mastodon adalah sebuah jejaring sosial <em>terbuka, open-source</em. Sebuah alternatif <em>desentralisasi</em> dari platform komersial, menjauhkan anda resiko dari sebuah perusahaan yang memonopoli komunikasi anda. Pilih server yang anda percayai &mdash; apapun yang anda pilih, anda tetap dapat berinteraksi dengan semua orang. Semua orang dapat menjalankan server Mastodon sendiri dan berpartisipasi dalam <em>jejaring sosial</em> dengan mudah.
     about_this: Tentang server ini
-    apps: Apl
     business_email: 'E-mail bisnis:'
     closed_registrations: Pendaftaran untuk server ini sedang ditutup.
     contact: Kontak
     description_headline: Apa itu %{domain}?
     domain_count_after: server lain
     domain_count_before: Terhubung dengan
-    features:
-      api: API terbuka untuk aplikasi dan layanan lain
-      blocks: Aneka ragam fitur blokir dan pembisuan
-      characters: 500 karakter per posting
-      chronology: Linimasa berurutan
-      ethics: 'Desain etis: tanpa iklan, tidak ada pelacakan'
-      gifv: Fitur GIFV dan video pendek
-      privacy: Terperinci, pengaturan privasi per postingan
-      public: Linimasa publik
-    features_headline: Yang berbeda dari Mastodon
     get_started: Mulai
     links: Link
     other_instances: Server lain
@@ -158,8 +147,6 @@ id:
           desc_html: Ditampilkan pada halaman depan saat pendaftaran ditutup<br>Anda bisa menggunakan tag HTML
           title: Pesan penutupan pendaftaran
         open:
-          disabled: Dinonaktifkan
-          enabled: Diaktifkan
           title: Pendaftaran terbuka
       setting: Pengaturan
       site_description:
diff --git a/config/locales/io.yml b/config/locales/io.yml
index db430b0fe..8eb48c303 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -3,23 +3,12 @@ io:
   about:
     about_mastodon: Mastodon esas <em>gratuita, apertitkodexa</em> sociala reto. Ol esas <em>sencentra</em> altra alternativo a komercala servadi. Ol evitigas, ke sola firmo guvernez tua tota komunikadol. Selektez servero, quan tu fidas. Irge qua esas tua selekto, tu povas komunikar kun omna altra uzeri. Irgu povas krear sua propra instaluro di Mastodon en sua servero, e partoprenar en la <em>sociala reto</em> tote glate.
     about_this: Pri ta instaluro
-    apps: Apliki
     business_email: 'Profesionala retpost-adreso:'
     closed_registrations: Membresko ne nun esas posible en ta instaluro.
     contact: Kontaktar
     description_headline: Quo esas %{domain}?
     domain_count_after: altra instaluri
     domain_count_before: Konektita ad
-    features:
-      api: Apertita API por apliki e servadi
-      blocks: Kompleta utensili por blokusar e celar
-      characters: Til 500 signi por singla mesajo
-      chronology: Tempolinei seguntempa
-      ethics: 'Etike kreita: nula anunco, nula trakado'
-      gifv: Posibleso diskononigar mikra videi e GIFV
-      privacy: Videbleso ajustita segun la mesajo
-      public: Publika tempolinei
-    features_headline: Quale Mastodon esas diferanta
     get_started: Komencar
     links: Ligili
     other_instances: Altra instaluri
@@ -146,8 +135,6 @@ io:
           desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags
           title: Closed registration message
         open:
-          disabled: Disabled
-          enabled: Enabled
           title: Open registration
       setting: Setting
       site_description:
diff --git a/config/locales/it.yml b/config/locales/it.yml
index a96a459df..5c014c61d 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -3,23 +3,12 @@ it:
   about:
     about_mastodon: Mastodon è un social network <em>gratuito e open-source</em>. Un'alternativa <em>decentralizzata</em> alle piattaforme commerciali che evita che una singola compagnia monopolizzi il tuo modo di comunicare. Scegli un server di cui ti fidi &mdash; qualunque sia la tua scelta, potrai interagire con chiunque altro. Chiunque può sviluppare un suo server Mastodon e partecipare alla vita del <em>social network</em>.
     about_this: A proposito di questo server
-    apps: Applicazioni
     business_email: 'Email di lavoro:'
     closed_registrations: Al momento le iscrizioni a questo server sono chiuse.
     contact: Contatti
     description_headline: Cos'è %{domain}?
     domain_count_after: altri server
     domain_count_before: Connesso a
-    features:
-      api: API aperto per applicazioni e servizi
-      blocks: Potenti strumenti di blocco e silenziamento
-      characters: 500 caratteri per status
-      chronology: Le timeline sono cronologiche
-      ethics: 'Design etico: niente pubblicità, niente tracking'
-      gifv: Set di GIFV e brevi video
-      privacy: Opzioni di privacy mirate per-post
-      public: Timeline pubbliche
-    features_headline: Cosa rende Mastodon migliore
     get_started: Inizia
     links: Links
     other_instances: Altri server
@@ -93,7 +82,7 @@ it:
       blocking: Lista dei bloccati
       following: Lista dei seguaci
     upload: Carica
-  landing_strip_html: <strong>%{name}</strong> è un utente su %{link_to_root_path}. Puoi seguirlo o interagire con lui se possiedi un account ovunque nel fediverse.
+  landing_strip_html: "<strong>%{name}</strong> è un utente su %{link_to_root_path}. Puoi seguirlo o interagire con lui se possiedi un account ovunque nel fediverse."
   landing_strip_signup_html: Se non possiedi un account, puoi <a href="%{sign_up_path}">iscriverti qui</a>.
   media_attachments:
     validations:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 9cb2428f7..5b91aa75d 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -3,23 +3,12 @@ ja:
   about:
     about_mastodon: Mastodon は<em>自由でオープンソース</em>なソーシャルネットワークです。商用プラットフォームの代替となる<em>分散型</em>を採用し、あなたのやりとりが一つの会社によって独占されるのを防ぎます。信頼できるインスタンスを選択してください &mdash; どのインスタンスを選んでも、誰とでもやりとりすることができます。 だれでも自分の Mastodon インスタンスを作ることができ、シームレスに<em>ソーシャルネットワーク</em>に参加できます。
     about_this: このインスタンスについて
-    apps: アプリ
     business_email: 'ビジネスメールアドレス:'
     closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。
     contact: 連絡先
     description_headline: "%{domain} とは?"
     domain_count_after: 個のインスタンス
     domain_count_before: 接続中
-    features:
-      api: アプリやその他サービスにAPIを公開
-      blocks: 豊富なブロックやミュート機能
-      characters: 1つの投稿は500文字まで利用可能
-      chronology: 時系列順のタイムライン
-      ethics: 広告もトラッキングもありません
-      gifv: GIFVや短い動画にも対応
-      privacy: 投稿ごとに公開範囲を細かく設定可能
-      public: 公開タイムライン
-    features_headline: Mastodon の特徴
     get_started: 参加する
     links: リンク
     other_instances: 他のインスタンス
@@ -173,8 +162,6 @@ ja:
           desc_html: 新規登録を停止しているときにフロントページに表示されます。<br>HTMLタグが利用可能です。
           title: 新規登録停止時のメッセージ
         open:
-          disabled: 無効
-          enabled: 有効
           title: 新規登録を受け付ける
       setting: 設定
       site_description:
@@ -395,7 +382,7 @@ ja:
       <h2>プライバシーポリシー</h2>
 
       <h3 id="collect">どのような情報を収集するのですか?</h3>
- 
+
       <p>あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。</p>
 
       <p>このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。</p>
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 844e78908..c8ad38d41 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -3,23 +3,12 @@ ko:
   about:
     about_mastodon: Mastodon 은<em>자유로운 오픈 소스</em>소셜 네트워크입니다. 상용 플랫폼의 대체로써 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 &mdash; 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 <em>소셜 네트워크</em>에 참가할 수 있습니다.
     about_this: 이 인스턴스에 대해서
-    apps: 어플리케이션
     business_email: '비즈니스 메일 주소:'
     closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다.
     contact: 연락처
     description_headline: "%{domain} 는 무엇인가요?"
     domain_count_after: 개의 인스턴스
     domain_count_before: 연결됨
-    features:
-      api: 어플리케이션이나 그 외 서비스에 API를 공개
-      blocks: 강력한 차단 및 뮤트 기능
-      characters: 한번에 500자까지 포스팅 가능
-      chronology: 시간 순서의 타임라인
-      ethics: 광고도 트래킹도 없습니다
-      gifv: GIFV나 짧은 동영상도 지원
-      privacy: 각 포스팅마다 공개 범위를 상세히 설정 가능
-      public: 공개 타임라인
-    features_headline: Mastodon 의 특징
     get_started: 참가하기
     links: 링크
     other_instances: 다른 인스턴스
@@ -151,7 +140,7 @@ ko:
       nsfw:
         'false': NSFW 꺼짐
         'true': NSFW 켜짐
-      report: "신고 #%{id}"
+      report: '신고 #%{id}'
       report_contents: 내용
       reported_account: 신고 대상 계정
       reported_by: 신고자
@@ -173,8 +162,6 @@ ko:
           desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다. <br>HTML 태그를 사용할 수 있습니다.
           title: 신규 등록 정지 시 메시지
         open:
-          disabled: 꺼짐
-          enabled: 켜짐
           title: 신규 등록을 받음
       setting: 설정
       site_description:
@@ -301,7 +288,7 @@ ko:
         one: "1건의 새로운 알림 \U0001F418"
         other: "%{count}건의 새로운 알림 \U0001F418"
     favourite:
-      body: '%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다.'
+      body: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다."
       subject: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다"
     follow:
       body: "%{name} 님이 나를 팔로우 했습니다"
@@ -323,7 +310,7 @@ ko:
     acct: 아이디@도메인을 입력해 주십시오
     missing_resource: 리디렉션 대상을 찾을 수 없습니다
     proceed: 팔로우 하기
-    prompt: '팔로우 하려 하고 있습니다'
+    prompt: 팔로우 하려 하고 있습니다
   sessions:
     activity: 마지막 활동
     browser: 브라우저
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 306ce6b1f..633061b06 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -3,23 +3,12 @@ nl:
   about:
     about_mastodon: Mastodon is een <em>vrij, gratis en open-source</em> sociaal netwerk. Een <em>gedecentraliseerd</em> alternatief voor commerciële platforms. Het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt &mdash; welke je ook kiest, je kunt met elke andere server communiceren. Iedereen kan een eigen Mastodon-server draaien en naadloos deelnemen in het <em>sociale netwerk</em>.
     about_this: Over deze server
-    apps: Apps
     business_email: 'E-mailadres:'
     closed_registrations: Registreren op deze server is momenteel uitgeschakeld.
     contact: Contact
     description_headline: Wat is %{domain}?
     domain_count_after: andere servers
     domain_count_before: Verbonden met
-    features:
-      api: Open API voor apps en diensten
-      blocks: Uitgebreide blokkeer- en negeerhulpmiddelen
-      characters: 500 tekens per bericht
-      chronology: Tijdlijnen zijn chronologisch
-      ethics: 'Ethisch design: geen advertenties, geen spionage'
-      gifv: GIFV-sets en korte video's
-      privacy: Nauwkeurige privacyinstellingen per toot (bericht)
-      public: Openbare tijdlijnen
-    features_headline: Wat maakt Mastodon anders
     get_started: Beginnen
     links: Links
     other_instances: Andere servers
@@ -73,8 +62,6 @@ nl:
           desc_html: Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld<br>En ook hier kan je HTML gebruiken
           title: Bericht wanneer registratie is uitgeschakeld
         open:
-          disabled: Uitgeschakeld
-          enabled: Ingeschakeld
           title: Open registratie
       setting: Instelling
       site_description:
@@ -89,7 +76,7 @@ nl:
   admin_mailer:
     new_report:
       body: "%{reporter} heeft %{target} gerapporteerd"
-      subject: Nieuwe toots gerapporteerd op %{instance} (#%{id})     
+      subject: Nieuwe toots gerapporteerd op %{instance} (#%{id})
   application_mailer:
     settings: 'E-mailvoorkeuren wijzigen: %{link}'
     signature: Mastodon-meldingen van %{instance}
@@ -131,7 +118,7 @@ nl:
     bad_password_msg: Goed geprobeerd hackers! Ongeldig wachtwoord
     confirm_password: Voer jouw huidige wachtwoord in om jouw identiteit te bevestigen
     description_html: Hierdoor worden alle gegevens van jouw account <strong>permanent, onomkeerbaar</strong> verwijderd en wordt deze gedeactiveerd. Om toekomstige identiteitsdiefstal te voorkomen, kan op deze server jouw gebruikersnaam niet meer gebruikt worden.
-    proceed: Account verwijderen 
+    proceed: Account verwijderen
     success_msg: Jouw account is succesvol verwijderd
     warning_html: We kunnen alleen garanderen dat jouw gegevens op deze server worden verwijderd. Berichten (toots), incl. media, die veel zijn gedeeld laten mogelijk sporen achter. Offline servers en servers die niet meer op jouw updates zijn geabonneerd zullen niet hun databases updaten.
     warning_title: Verwijdering gegevens op andere servers
@@ -177,7 +164,7 @@ nl:
       following: Volglijst
       muting: Negeerlijst
     upload: Uploaden
-  landing_strip_html: <strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt.
+  landing_strip_html: "<strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt."
   landing_strip_signup_html: Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier registreren</a>.
   notification_mailer:
     digest:
@@ -286,7 +273,7 @@ nl:
     generate_recovery_codes: Herstelcodes genereren
     instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op jouw mobiele telefoon</strong>. Van nu af aan genereert deze app aanmeldcodes die je bij het aanmelden moet invoeren."
     lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig.
-    manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.'
+    manual_instructions: Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.
     recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd
     recovery_instructions_html: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.)
     setup: Instellen
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 5fd63f121..05714959d 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -3,23 +3,12 @@
   about:
     about_mastodon: Mastodon er et sosialt nettverk laget med <em>fri programvare</em>. Et <em>desentralisert</em> alternativ til kommersielle plattformer. Slik kan det unngå risikoene ved å ha et enkelt selskap som monopoliserer din kommunikasjon. Velg en tjener du stoler på &mdash; uansett hvilken du velger så kan du kommunisere med alle andre. Alle kan kjøre sin egen Mastodon og delta sømløst i det sosiale nettverket.
     about_this: Om denne instansen
-    apps: Applikasjoner
     business_email: 'Bedriftsepost:'
     closed_registrations: Registreringer er for øyeblikket lukket på denne instansen.
     contact: Kontakt
     description_headline: Hva er %{domain}?
     domain_count_after: andre instanser
     domain_count_before: Koblet til
-    features:
-      api: Åpent API for applikasjoner og tjenester
-      blocks: Rikholdige blokkeringsverktøy
-      characters: 500 tegn per status
-      chronology: Tidslinjer er kronologiske
-      ethics: 'Etisk design: Ingen reklame, ingen sporing'
-      gifv: Støtte for GIFV og korte videoer
-      privacy: Finmaskede personvernsinnstillinger
-      public: Felles tidslinjer
-    features_headline: Hva skiller Mastodon fra andre sosiale nettverk
     get_started: Kom i gang
     links: Lenker
     other_instances: Andre instanser
@@ -160,8 +149,6 @@
           desc_html: Vises på forsiden når registreringer er lukket<br>Du kan bruke HTML-tagger
           title: Melding for lukket registrering
         open:
-          disabled: På
-          enabled: Av
           title: Åpen registrering
       setting: Innstilling
       site_description:
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 631133f74..30d5258a5 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -3,23 +3,12 @@ oc:
   about:
     about_mastodon: Mastodon es un malhum social <em>liure e open-source</em>. Una alternativa <em>decentralizada</em> a las platformas comercialas, aquò evita qu’una sola companiá monopolize vòstra comunicacion. Causissètz une servidor que vos fisatz, quina que siague vòstra causida, podètz interagir amb tot lo mond. Qual que siague pòt aver son instància Mastodon e participar al <em>malhum social</em> sens cap de problèmas.
     about_this: A prepaus d’aquesta instància
-    apps: Aplicacions
     business_email: 'Corrièl professional :'
     closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància.
     contact: Contacte
     description_headline: Qué es %{domain} ?
     domain_count_after: autras instàncias
     domain_count_before: Connectat a
-    features:
-      api: API dubèrta per las aplicacions e servicis
-      blocks: Aisinas complètas per blocar e rescondre
-      characters: 500 caractèrs per publicacion
-      chronology: Flux d’actualitat cronologic
-      ethics: 'Ethical design: pas cap de reclama o traçador'
-      gifv: Partatge de GIFs e vidèos cortas
-      privacy: Nivèl de confidencialitat configurable per cada publicacion
-      public: Fluxes d’actualitat publics
-    features_headline: Çò que fa que Mastodon es diferent
     get_started: Venètz al malhum
     links: Ligams
     other_instances: Autras instàncias
@@ -167,8 +156,6 @@ oc:
           desc_html: Afichat sus las pagina d’acuèlh quand las inscripcions son tampadas.<br>Podètz utilizar de balisas HTML
           title: Messatge de barradura de las inscripcions
         open:
-          disabled: Desactivadas
-          enabled: Activadas
           title: Inscripcions
       setting: Paramètre
       site_description:
@@ -220,7 +207,7 @@ oc:
     - dv
     - ds
     abbr_month_names:
-    -
+    - 
     - gen
     - feb
     - mar
@@ -246,7 +233,7 @@ oc:
       long: Lo %e %B de %Y
       short: "%e %b"
     month_names:
-    -
+    - 
     - de genièr
     - de febrièr
     - de març
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 321b1590e..018ff3c7b 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -3,23 +3,12 @@ pl:
   about:
     about_mastodon: Mastodon jest <em>wolną i otwartą</em> siecią społecznościową, <em>zdecentralizowaną</em> alternatywą dla zamkniętych, komercyjnych platform. Pozwala uniknąć ryzyka monopolizacji Twojej komunikacji przez jedną korporację. Wybierz serwer, któremu ufasz &mdash; nie ograniczy to Twoich możliwości komunikacji z innymi osobami w sieci. Każdy może też uruchomić własną instancję Mastodona i dołączyć do reszty tej <em>sieci społecznościowej</em>.
     about_this: O tej instancji
-    apps: Aplikacje
     business_email: 'Służbowy adres e-mail:'
     closed_registrations: Rejestracja na tej instancji jest obecnie zamknięta.
     contact: Kontakt
     description_headline: Czym jest %{domain}?
     domain_count_after: instancji
     domain_count_before: Serwer połączony z
-    features:
-      api: Otwarte API dla aplikacji i usług
-      blocks: Rozbudowane narzędzia blokowania i ukrywania
-      characters: 500 znaków na wpis
-      chronology: Chronologiczny porządek wyświetlania
-      ethics: 'Etyczne założenia: nie śledzimy, bez reklam'
-      gifv: obsługa GIFV i krótkich wideo
-      privacy: Precyzyjne ustawienia widoczności poszczególnych postów
-      public: Publiczne osie czasu
-    features_headline: Co wyróżnia Mastodona
     get_started: Rozpocznijmy!
     links: Odnośniki
     other_instances: Inne instancje
@@ -48,7 +37,7 @@ pl:
       create:
         name: "%{account_name} utworzył(a) wpis."
     outbox:
-      name: "Skrzynka %{account_name}"
+      name: Skrzynka %{account_name}
       summary: Zbiór aktywności użytkownika %{account_name}.
   admin:
     accounts:
@@ -173,8 +162,6 @@ pl:
           desc_html: Wyświetlana na stronie głównej, gdy możliwość otwarej rejestracji<br>nie jest dostępna. Możesz korzystać z tagów HTML
           title: Wiadomość o nieaktywnej rejestracji
         open:
-          disabled: Nieaktywna
-          enabled: Aktywna
           title: Otwarta rejestracja
       setting: Ustawienie
       site_description:
@@ -198,7 +185,7 @@ pl:
     title: Administracja
   admin_mailer:
     new_report:
-      body: "Użytkownik %{reporter} zgłosił %{target}"
+      body: Użytkownik %{reporter} zgłosił %{target}
       subject: Nowe zgłoszenie na %{instance} (#%{id})
   application_mailer:
     settings: 'Zmień ustawienia powiadamiania: %{link}'
@@ -469,7 +456,7 @@ pl:
       <p>Dokument jest dostępny na licencji CC-BY-SA. Ostatnio modyfikowany 31 maja 2013, przetłumaczony 4 lipca 2017. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.</p>
 
       <p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.
-    title: "Zasady korzystania i polityka prywatności %{instance}"
+    title: Zasady korzystania i polityka prywatności %{instance}
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 973a8d401..fb5e03c8c 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -3,23 +3,12 @@ pt-BR:
   about:
     about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie &mdash; qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
     about_this: Sobre essa instância
-    apps: Aplicações
     business_email: 'Email comercial:'
     closed_registrations: Registros estão fechadas para essa instância.
     contact: Contato
     description_headline: O que é %{domain}?
     domain_count_after: outras instâncias
     domain_count_before: Conectado a
-    features:
-      api: Aberto para API de aplicações e serviços
-      blocks: Bloqueos e ferramentas para mudar
-      characters: 500 caracteres por post
-      chronology: Timeline são cronologicas
-      ethics: 'Design ético: sem propaganda, sem tracking'
-      gifv: GIFV e vídeos curtos
-      privacy: Granular, privacidade setada por post
-      public: Timelines públicas
-    features_headline: O que torna Mastodon diferente
     get_started: Comece aqui
     links: Links
     other_instances: Outras instâncias
@@ -159,8 +148,6 @@ pt-BR:
           desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML
           title: Mensagem de registro fechados
         open:
-          disabled: Desabilitado
-          enabled: Habilitado
           title: Aberto para registro
       setting: Preferências
       site_description:
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index dff2898c0..0f59185a7 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -3,23 +3,12 @@ pt:
   about:
     about_mastodon: Mastodon é uma rede social <em>grátis e em código aberto</em>. Uma alternativa <em>descentralizada</em> às plataformas comerciais, que evita o risco de uma única empresa monopolizar a tua comunicação. Escolhe um servidor que confies, não importa qual, pois vais poder comunicar com todos os outros. Qualquer um pode criar uma instância Mastodon e participar nesta <em>rede social</em>.
     about_this: Sobre esta instância
-    apps: Aplicações
     business_email: 'Email comercial:'
     closed_registrations: Novos registos estão fechados nesta instância.
     contact: Contacto
     description_headline: O que é o %{domain}?
     domain_count_after: outras instâncias
     domain_count_before: Ligado a
-    features:
-      api: API aberta para aplicações e serviços
-      blocks: Ferramentas para silenciar e bloquear
-      characters: 500 caracteres por post
-      chronology: Timelines cronológicas
-      ethics: 'Design ético: sem públicidade ou tracking'
-      gifv: GIFV e pequenos vídeos
-      privacy: Privacidade granular por post
-      public: Timelines públicas
-    features_headline: O que torna Mastodon diferente
     get_started: Começar
     links: Links
     other_instances: Outras instâncias
@@ -154,8 +143,6 @@ pt:
           desc_html: Mostrar na página inicial quando registos estão encerrados<br/>Podes usar tags HTML
           title: Mensagem de registos encerrados
         open:
-          disabled: Desabilitado
-          enabled: Habilitado
           title: Aceitar novos registos
       setting: Preferências
       site_description:
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 6321e96eb..414d39dd2 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -3,23 +3,12 @@ ru:
   about:
     about_mastodon: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете &mdash; что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно.
     about_this: Об этом узле
-    apps: Приложения
     business_email: 'Деловой e-mail:'
     closed_registrations: В данный момент регистрация на этом узле закрыта.
     contact: Связаться
     description_headline: Что такое %{domain}?
     domain_count_after: другими узлами
     domain_count_before: Связан с
-    features:
-      api: Открытый API для приложений и сервисов
-      blocks: Продвинутые инструменты блокирования и глушения
-      characters: 500 символов на пост
-      chronology: Хронологические ленты
-      ethics: 'Этичный дизайн: нет рекламы, нет слежения'
-      gifv: GIFV и короткие видео
-      privacy: Тонкие настройки приватности для каждого поста
-      public: Публичные ленты
-    features_headline: Что выделяет Mastodon
     get_started: Начать
     links: Ссылки
     other_instances: Другие узлы
@@ -141,8 +130,6 @@ ru:
           desc_html: Отображается на титульной странице, когда закрыта регистрация<br>Можно использовать HTML-теги
           title: Сообщение о закрытой регистрации
         open:
-          disabled: Закрыта
-          enabled: Открыта
           title: Открыть регистрацию
       setting: Настройка
       site_description:
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 322e5e74b..a71252afe 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -3,23 +3,12 @@ th:
   about:
     about_mastodon: แมสโทดอน เป็น  <em>ดีเซ็นทรัลไลซ์</em><em>ฟรีโอเพ่นซอร์ส</em> โซเชี่ยวเน็ตเวริ์ค.  เป็นทางเลือกทดแทนโซเชี่ยวเน็ตเวิร์คที่ทำเป็นธุรกิจการค้า, ป้องกันการผูกขาดช่องทางการสื่อสารของคุณ. เลือกเซร์ฟเวอร์ที่คุณไว้ใจ &mdash; ที่คุณเลือกได้เอง, สื่อสารกับคนที่คุณต้องการได้เสมอ. ใครๆก็รันแมสโทดอนอินซะแตนซ์ได้ และ เชื่อมต่อกับ<em>โซเชี่ยวเน็ตเวิร์ค</em> โดยไม่มีอะไรมาขวางกั้น.
     about_this: เกี่ยวกับอินซะแตนซ์นี้
-    apps: แอ๊ฟ
     business_email: 'อีเมล์ธุรกิจ:'
     closed_registrations: อินซะแตนซ์นี้ปิดรับลงทะเบียนแล้ว.
     contact: ติดต่อ
     description_headline: โดเมนคือ %{domain} ?
     domain_count_after: อินซะแตนซ์อื่นๆ
     domain_count_before: เชื่อมต่อกับ
-    features:
-      api: API เปิดสำหรับ Apps และ Services
-      blocks: มีเครื่องมือสำหรับ Block และ Mute
-      characters: เขียนได้ 500 ตัวอักษรต่อโพสต์
-      chronology: Timelines are chronological
-      ethics: 'ออกแบบด้วยจรรยาบรรณ: ไม่มีโฆษณา, ไม่มีการแทรค'
-      gifv: รองรับภาพ GIFV และ วีดีโอสั้น
-      privacy: Granular, per-post privacy settings
-      public: ไทม์ไลน์สาธารณะ
-    features_headline: What sets Mastodon apart
     get_started: เริ่มกันเลย
     links: ลิงก์
     other_instances: อินซะแตนซ์อื่นๆ
@@ -160,8 +149,6 @@ th:
           desc_html: Displayed on frontpage when registrations are closed<br> ใช้ HTML tags ได้
           title: ปิดข้อความลงทะเบียน
         open:
-          disabled: ปิดการใช้งาน
-          enabled: ปิดใช้งาน
           title: เปิดรับลงทะเบียน
       setting: ตั้งค่า
       site_description:
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 0e33e2efe..a8927eaa8 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -3,23 +3,12 @@ tr:
   about:
     about_mastodon: Mastodon <em>ücretsiz ve açık kaynaklı</em> bir sosyal ağdır. <em>Merkezileştirilmemiş</em> yapısı sayesinde diğer ticari sosyal platformların aksine iletişimininizin tek bir firmada tutulmasının/yönetilmesinin önüne geçer. Güvendiğiniz bir sunucuyu seçerek oradaki kişilerle etkileşimde bulunabilirsiniz. Herkes kendi Mastodon sunucusunu kurabilir ve sorunsuz bir şekilde Mastodon <em>sosyal ağına</em> dahil edebilir.
     about_this: Bu sunucu hakkında
-    apps: Uygulamalar
     business_email: 'İş e-postası:'
     closed_registrations: Bu sunucu şu anda yeni kayıt almamaktadır.
     contact: İletişim
     description_headline: Peki %{domain} nedir?
     domain_count_after: sunucu var.
     domain_count_before: Bağlı olduğu
-    features:
-      api: Uygulama ve servisler için açık API
-      blocks: Zengin blok ve iletişim araçları
-      characters: 500 karakterlik gönderiler
-      chronology: Kronolojik zaman tüneli
-      ethics: 'Etik tasarım: reklam ve izleme yok'
-      gifv: GIFV ve diğer video türleri
-      privacy: Gönderi bazlı gizlilik
-      public: Herkese açık zaman tünelleri
-    features_headline: Mastodon'ı diğerlerinden ayıran nedir?
     get_started: Kayıt ol
     links: Bağlantılar
     other_instances: Diğer sunucular
@@ -159,8 +148,6 @@ tr:
           desc_html: Kayıt alımları kapatıldığında ana sayfada görüntülenecek mesajdır. <br> HTML etiketleri kullanabilirsiniz.
           title: Kayıt alımları kapatılma mesajı
         open:
-          disabled: Kapalı
-          enabled: Açık
           title: Kayıt alımları
       setting: Ayar adı
       site_description:
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 1327c1a7b..65f1aabf5 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -3,23 +3,12 @@ uk:
   about:
     about_mastodon: Mastodon - це <em>вільна</em> соціальна мережа з <em>відкритим вихідним кодом</em>. Вона є <em>децентралізованою</em> альтернативою комерційним платформам, що дозволяє уникнути ризиків монополізації вашого спілкування однією компанією. Виберіть сервер, якому ви довіряєте &mdash; що б ви не вибрали, Ви зможете спілкуватись з усіма іншими. Будь-який користувач може запустити власну інстанцію Mastodon та без проблем брати участь в <em>соціальній мережі</em>.
     about_this: Про цю інстанцію
-    apps: Додатки
     business_email: 'Діловий email:'
     closed_registrations: На даний момент реєстрація на цій інстанції закрита.
     contact: Зв'язатися
     description_headline: Що таке %{domain}?
     domain_count_after: іншими інстанціями
     domain_count_before: Зв'язаний з
-    features:
-      api: Відкритий API для додаків та сервісів
-      blocks: Продвинуті інструменти самомодерації
-      characters: 500 символів на пост
-      chronology: Хронологічні стрічки
-      ethics: 'Этичний дизайн: немає реклами, немає стеження'
-      gifv: GIFV та короткі відео
-      privacy: Тонкі налаштування приватності для кожного поста
-      public: Публічні стрічки
-    features_headline: Що виділяє Mastodon
     get_started: Почати
     links: Посилання
     other_instances: Інші інстанції
@@ -141,8 +130,6 @@ uk:
           desc_html: Відображається на титульній сторінці, коли реєстрація закрита <br>Можна використовувати HTML-теги
           title: Повідомлення про закриту реєстрацію
         open:
-          disabled: Закрита
-          enabled: Відкрита
           title: Відкрити реєстрацію
       setting: Налаштування
       site_description:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 6c8e9fc6d..d5d1b672c 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -3,23 +3,12 @@ zh-CN:
   about:
     about_mastodon: Mastodon(长毛象)是一个<em>自由、开放源码</em>的社交网站。它是一个分布式的服务,避免你的通信被单一商业机构垄断操控。请你选择一家你信任的 Mastodon 实例,在上面创建帐号,然后你就可以和任一 Mastodon 实例上的用户互通,享受无缝的<em>社交</em>交流。
     about_this: 关于本实例
-    apps: 应用程序
     business_email: 商业电邮︰
     closed_registrations: 这个实例目前不开放注册 _(:3」∠)_
     contact: 联络
     description_headline: 关于 %{domain}
     domain_count_after: 个其它实例
     domain_count_before: 现已接入
-    features:
-      api: 开放 API,供各式应用程序及服务接入
-      blocks: 完善的封锁用户、静音功能
-      characters: 每篇嘟文最多 500 字
-      chronology: 纯粹按时间排序,不作多余处理
-      ethics: 良心设计︰没有广告,不追踪你的使用行为
-      gifv: 支持显示 GIFV 动图小视频
-      privacy: 可逐篇嘟文设置隐私
-      public: 提供公共时间轴
-    features_headline: 是什么让 Mastodon 与众不同
     get_started: 上手使用
     links: 链接
     other_instances: 其它实例
@@ -166,8 +155,6 @@ zh-CN:
           desc_html: 当本站暂停接受注册时,会显示这个消息。<br/> 可使用 HTML
           title: 暂停注册消息
         open:
-          disabled: 停用
-          enabled: 启用
           title: 开放注册
       setting: 设置
       site_description:
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 4d8262c5b..aa9f27912 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -3,23 +3,12 @@ zh-HK:
   about:
     about_mastodon: Mastodon(萬象)是<em>自由、開源</em>的社交網絡。服務站<em>各自獨立而互連</em>,避免單一商業機構壟斷。找你所信任的服務站,建立帳號,你即可與任何服務站上的用戶溝通,享受無縫的<em>網絡交流</em>。
     about_this: 關於本服務站
-    apps: 應用程式
     business_email: 聯絡網站管理者︰
     closed_registrations: 本服務站暫時停止接受登記。
     contact: 聯絡
     description_headline: 關於 %{domain}
     domain_count_after: 個其他服務站
     domain_count_before: 已連接至
-    features:
-      api: 開放 API,供各式應用程式及服務連入
-      blocks: 完善的封鎖用戶、靜音功能
-      characters: 每篇文章最多 500 字
-      chronology: 時間軸忠實按時排序,不多餘處理
-      ethics: 良心設計︰無廣告,不追蹤用戶
-      gifv: 支援顯示 GIFV 短片圖組
-      privacy: 可逐篇文章設定私隱度
-      public: 公共時間軸
-    features_headline: 甚麼讓 Mastodon 與眾不同
     get_started: 立即登記
     links: 連結
     other_instances: 其他服務站
@@ -159,8 +148,6 @@ zh-HK:
           desc_html: 當本站暫停接受註冊時,會顯示這個訊息。<br/> 可使用 HTML
           title: 暫停註冊訊息
         open:
-          disabled: 停用
-          enabled: 啟用
           title: 開放註冊
       setting: 設定
       site_description:
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 754175a7a..58caf7848 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -3,23 +3,12 @@ zh-TW:
   about:
     about_mastodon: Mastodon (長毛象)是一個<em>自由、開放原始碼</em>的社群網站。它是一個分散式的服務,避免您的通訊被單一商業機構壟斷操控。請您選擇一家您信任的 Mastodon 服務站,在上面建立帳號,然後您就可以和任一 Mastodon 服務站上的使用者互通,享受無縫的<em>社群網路</em>交流。
     about_this: 關於本服務站
-    apps: 應用程式
     business_email: 商務信箱︰
     closed_registrations: 本服務站暫時停止接受註冊。
     contact: 聯絡我們
     description_headline: 關於 %{domain}
     domain_count_after: 個服務站相連
     domain_count_before: 與其他
-    features:
-      api: 開放 API,供各式應用程式及服務串接
-      blocks: 完善的封鎖使用者、靜音功能
-      characters: 每篇文章最多 500 字
-      chronology: 時間軸按時序顯示文章,不作多餘處理
-      ethics: 良心設計︰沒有廣告,不追蹤您的使用行為
-      gifv: 支援顯示 GIFV 短片
-      privacy: 可逐篇文章調整隱私設定
-      public: 公開時間軸
-    features_headline: Mastodon 與眾不同之處
     get_started: 立即註冊
     links: 連結
     other_instances: 其他服務站
@@ -120,8 +109,6 @@ zh-TW:
           desc_html: 關閉註冊時顯示在首頁的內容,可使用 HTML 標籤。
           title: 關閉註冊訊息
         open:
-          disabled: 停用
-          enabled: 啟用
           title: 開放註冊
       setting: 設定
       site_description:
diff --git a/config/settings.yml b/config/settings.yml
index be2a7a1f8..579ba2929 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -16,6 +16,7 @@ defaults: &defaults
   open_registrations: true
   closed_registrations_message: ''
   open_deletion: true
+  timeline_preview: true
   boost_modal: false
   auto_play_gif: false
   delete_modal: true
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 0e182c755..bd70937e4 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -161,16 +161,12 @@ namespace :mastodon do
   namespace :settings do
     desc 'Open registrations on this instance'
     task open_registrations: :environment do
-      setting = Setting.where(var: 'open_registrations').first
-      setting.value = true
-      setting.save
+      Setting.open_registrations = true
     end
 
     desc 'Close registrations on this instance'
     task close_registrations: :environment do
-      setting = Setting.where(var: 'open_registrations').first
-      setting.value = false
-      setting.save
+      Setting.open_registrations = false
     end
   end
 
diff --git a/spec/requests/localization_spec.rb b/spec/requests/localization_spec.rb
index 2f7a5e91e..f625a93a4 100644
--- a/spec/requests/localization_spec.rb
+++ b/spec/requests/localization_spec.rb
@@ -6,13 +6,13 @@ describe 'Localization' do
   after(:all) do
     I18n.locale = I18n.default_locale
   end
-  
+
   it 'uses a specific region when provided' do
     headers = { 'Accept-Language' => 'zh-HK' }
 
     get "/about", headers: headers
     expect(response.body).to include(
-      I18n.t('about.about_mastodon', locale: 'zh-HK')
+      I18n.t('about.about_mastodon_html', locale: 'zh-HK')
     )
   end
 
@@ -21,7 +21,7 @@ describe 'Localization' do
 
     get "/about", headers: headers
     expect(response.body).to include(
-      I18n.t('about.about_mastodon', locale: 'es')
+      I18n.t('about.about_mastodon_html', locale: 'es')
     )
   end
   it 'falls back to english when locale is missing' do
@@ -29,7 +29,7 @@ describe 'Localization' do
 
     get "/about", headers: headers
     expect(response.body).to include(
-      I18n.t('about.about_mastodon', locale: 'en')
+      I18n.t('about.about_mastodon_html', locale: 'en')
     )
   end
 end
diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb
index 2c5130d84..c0ead6349 100644
--- a/spec/views/about/show.html.haml_spec.rb
+++ b/spec/views/about/show.html.haml_spec.rb
@@ -10,10 +10,11 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
 
   it 'has valid open graph tags' do
     instance_presenter = double(:instance_presenter,
-				site_description: 'something',
-				open_registrations: false,
-				closed_registrations_message: 'yes',
-			       )
+                                site_title: 'something',
+                                site_description: 'something',
+                                version_number: '1.0',
+                                open_registrations: false,
+                                closed_registrations_message: 'yes')
     assign(:instance_presenter, instance_presenter)
     render
 
-- 
cgit 


From 056b5ed72f6d980bceeb49eb249b8365fe8fce66 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 12 Jul 2017 03:24:04 +0200
Subject: Improve UI of admin site settings (#4163)

---
 app/controllers/admin/settings_controller.rb       |  6 +-
 app/javascript/styles/forms.scss                   | 87 +++++++++++++++------
 app/models/form/admin_settings.rb                  | 29 +++++++
 app/views/admin/settings/edit.html.haml            | 88 +++++++---------------
 config/initializers/simple_form.rb                 |  4 +-
 config/locales/ca.yml                              |  2 -
 config/locales/de.yml                              |  2 -
 config/locales/en.yml                              | 40 +++++-----
 config/locales/fa.yml                              |  2 -
 config/locales/fr.yml                              |  2 -
 config/locales/he.yml                              |  2 -
 config/locales/id.yml                              |  2 -
 config/locales/io.yml                              |  2 -
 config/locales/ja.yml                              |  2 -
 config/locales/ko.yml                              |  2 -
 config/locales/nl.yml                              |  2 -
 config/locales/no.yml                              |  2 -
 config/locales/oc.yml                              |  2 -
 config/locales/pl.yml                              |  8 +-
 config/locales/pt-BR.yml                           |  2 -
 config/locales/pt.yml                              |  2 -
 config/locales/ru.yml                              |  2 -
 config/locales/th.yml                              |  2 -
 config/locales/tr.yml                              |  2 -
 config/locales/uk.yml                              |  2 -
 config/locales/zh-CN.yml                           |  2 -
 config/locales/zh-HK.yml                           |  2 -
 config/locales/zh-TW.yml                           |  2 -
 spec/controllers/admin/settings_controller_spec.rb |  8 +-
 29 files changed, 151 insertions(+), 161 deletions(-)
 create mode 100644 app/models/form/admin_settings.rb

(limited to 'spec')

diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index 29b590d7a..5985d6282 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -22,7 +22,7 @@ module Admin
     ).freeze
 
     def edit
-      @settings = Setting.all_as_records
+      @admin_settings = Form::AdminSettings.new
     end
 
     def update
@@ -38,12 +38,12 @@ module Admin
     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/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index e723b50ff..e1de36d55 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -57,10 +57,7 @@ code {
     }
   }
 
-  .input.file,
-  .input.select,
-  .input.radio_buttons,
-  .input.check_boxes {
+  .input.with_label {
     padding: 15px 0;
     margin-bottom: 0;
 
@@ -71,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 {
@@ -106,7 +141,7 @@ code {
     input[type=checkbox] {
       position: absolute;
       left: 0;
-      top: 1px;
+      top: 5px;
       margin: 0;
     }
 
@@ -116,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],
@@ -390,25 +448,6 @@ code {
   }
 }
 
-.user_filtered_languages {
-  & > 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%;
-  }
-}
-
 .post-follow-actions {
   text-align: center;
   color: $ui-primary-color;
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/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 59192530b..9f8a6640b 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -1,64 +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.site_terms.title')
-          %p= t('admin.settings.site_terms.desc_html')
-        %td= text_area_tag :site_terms, @settings['site_terms'].value, rows: 8
-      %tr
-        %td
-          %strong= t('admin.settings.registrations.open.title')
-          %p= t('admin.settings.registrations.open.desc_html')
-        %td
-          = select_tag :open_registrations, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => 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
-      %tr
-        %td
-          %strong= t('admin.settings.registrations.deletion.title')
-          %p= t('admin.settings.registrations.deletion.desc_html')
-        %td
-          = select_tag :open_deletion, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['open_deletion'].value)
-      %tr
-        %td
-          %strong= t('admin.settings.timeline_preview.title')
-          %p= t('admin.settings.timeline_preview.desc_html')
-        %td
-          = select_tag :timeline_preview, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['timeline_preview'].value)
+= 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')
 
-  .simple_form.actions
-    = button_tag t('generic.save_changes'), type: :submit, class: :btn
+  %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/config/initializers/simple_form.rb b/config/initializers/simple_form.rb
index ca7531748..5983918cd 100644
--- a/config/initializers/simple_form.rb
+++ b/config/initializers/simple_form.rb
@@ -50,14 +50,14 @@ SimpleForm.setup do |config|
     # b.use :full_error, wrap_with: { tag: :span, class: :error }
   end
 
-  config.wrappers :with_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b|
+  config.wrappers :with_label, class: [:input, :with_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
     b.use :html5
     b.use :label_input, wrap_with: { tag: :div, class: :label_input }
     b.use :hint,  wrap_with: { tag: :span, class: :hint }
     b.use :error, wrap_with: { tag: :span, class: :error }
   end
 
-  config.wrappers :with_block_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b|
+  config.wrappers :with_block_label, class: [:input, :with_block_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
     b.use :html5
     b.use :label
     b.use :hint, wrap_with: { tag: :span, class: :hint }
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 10c34498b..f63aee3e6 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -155,7 +155,6 @@ ca:
     settings:
       contact_information:
         email: Introduir una adreça de correu electrònic pùblica
-        label: Informació de contacte
         username: Introduir un nom d'usuari
       registrations:
         closed_message:
@@ -163,7 +162,6 @@ ca:
           title: Missatge de registre tancat
         open:
           title: Registre obert
-      setting: Ajust
       site_description:
         desc_html: Es mostra com un paràgraf a la pàgina principal i s'utilitza com una etiqueta meta.<br>Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Descripció del lloc
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 2bdb87708..b084aca31 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -129,7 +129,6 @@ de:
     settings:
       contact_information:
         email: Eine öffentliche E-Mail-Adresse angeben
-        label: Kontaktinformationen
         username: Einen Benutzernamen angeben
       registrations:
         closed_message:
@@ -137,7 +136,6 @@ de:
           title: Nachricht über geschlossene Registrierung
         open:
           title: Offene Registrierung
-      setting: Einstellung
       site_description:
         desc_html: Wird als Absatz auf der Frontseite angezeigt und als Meta-Tag benutzt.<br>Du kannst HTML-Tags benutzen, insbesondere <code>&lt;a&gt;</code> und <code>&lt;em&gt;</code>.
         title: Seitenbeschreibung
diff --git a/config/locales/en.yml b/config/locales/en.yml
index e33dde038..c9b5d9ab8 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -168,33 +168,31 @@ en:
       view: View
     settings:
       contact_information:
-        email: Enter a public e-mail address
-        label: Contact information
-        username: Enter a username
+        email: Business e-mail
+        username: Contact username
       registrations:
         closed_message:
-          desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags
+          desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags
           title: Closed registration message
+        deletion:
+          desc_html: Allow anyone to delete their account
+          title: Open account deletion
         open:
           desc_html: Allow anyone to create an account
           title: Open registration
-        deletion:
-          desc_html: Allow anyone to delete their account
-          title: Open deletion
-      timeline_preview:
-        desc_html: Display public timeline on landing page
-        title: Timeline preview
-      setting: Setting
       site_description:
-        desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
-        title: Site description
+        desc_html: Introductory paragraph on the frontpage and in meta tags. You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
+        title: Instance description
       site_description_extended:
-        desc_html: Displayed on extended information page<br>You can use HTML tags
-        title: Extended site description
+        desc_html: A good place for your code of conduct, rules, guidelines and other things that set your instance apart. You can use HTML tags
+        title: Custom extended information
       site_terms:
-        desc_html: Displayed on terms page<br>You can use HTML tags
-        title: Privacy policy
-      site_title: Site title
+        desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
+        title: Custom terms of service
+      site_title: Instance name
+      timeline_preview:
+        desc_html: Display public timeline on landing page
+        title: Timeline preview
       title: Site Settings
     subscriptions:
       callback_url: Callback URL
@@ -230,12 +228,12 @@ en:
   authorize_follow:
     error: Unfortunately, there was an error looking up the remote account
     follow: Follow
-    following: 'Success! You are now following:'
     follow_request: 'You have sent a follow request to:'
+    following: 'Success! You are now following:'
     post_follow:
-      web: Go to web
-      return: Return to the user's profile
       close: Or, you can just close this window.
+      return: Return to the user's profile
+      web: Go to web
     prompt_html: 'You (<strong>%{self}</strong>) have requested to follow:'
     title: Follow %{acct}
   datetime:
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 6f0bd0839..ade76d670 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -141,7 +141,6 @@ fa:
     settings:
       contact_information:
         email: یک نشانی ایمیل عمومی وارد کنید
-        label: اطلاعات تماس
         username: یک نام کاربری وارد کنید
       registrations:
         closed_message:
@@ -149,7 +148,6 @@ fa:
           title: پیغام برای فعال‌نبودن ثبت نام
         open:
           title: امکان ثبت نام
-      setting: تنظیمات
       site_description:
         desc_html: روی صفحهٔ اصلی نمایش می‌یابد و همچنین به عنوان تگ‌های HTML.<br>می‌توانید HTML بنویسید, به‌ویژه <code>&lt;a&gt;</code> و <code>&lt;em&gt;</code>.
         title: دربارهٔ سایت
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 9eeafaa6e..c2efd0c85 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -155,7 +155,6 @@ fr:
     settings:
       contact_information:
         email: Entrez une adresse courriel publique
-        label: Informations de contact
         username: Entrez un nom d’utilisateur⋅ice
       registrations:
         closed_message:
@@ -163,7 +162,6 @@ fr:
           title: Message de fermeture des inscriptions
         open:
           title: Inscriptions
-      setting: Paramètre
       site_description:
         desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.<br>Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
         title: Description du site
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 760ddac00..21f8f1dc4 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -149,7 +149,6 @@ he:
     settings:
       contact_information:
         email: נא להקליד כתובת דוא"ל פומבית
-        label: פרטי התקשרות
         username: נא להכניס שם משתמש
       registrations:
         closed_message:
@@ -157,7 +156,6 @@ he:
           title: מסר סגירת הרשמות
         open:
           title: הרשמה פתוחה
-      setting: הגדרה
       site_description:
         desc_html: מוצג כפסקה על הדף הראשי ומשמש כתגית מטא.<br>ניתן להשתמש בתגיות HTML, ובמיוחד ב־<code>&lt;a&gt;</code> ו־<code>&lt;em&gt;</code>.
         title: תיאור האתר
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 4bcd8f2ed..e3fe96331 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -140,7 +140,6 @@ id:
     settings:
       contact_information:
         email: Masukkan alamat email
-        label: Informasi kontak
         username: Masukkan nama pengguna
       registrations:
         closed_message:
@@ -148,7 +147,6 @@ id:
           title: Pesan penutupan pendaftaran
         open:
           title: Pendaftaran terbuka
-      setting: Pengaturan
       site_description:
         desc_html: Ditampilkan sebagai sebuah paragraf di halaman depan dan digunakan sebagai tag meta.<br>Anda bisa menggunakan tag HTML, khususnya <code>&lt;a&gt;</code> dan <code>&lt;em&gt;</code>.
         title: Deskripsi situs
diff --git a/config/locales/io.yml b/config/locales/io.yml
index 8eb48c303..b587d4bc6 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -128,7 +128,6 @@ io:
     settings:
       contact_information:
         email: Enter a public e-mail address
-        label: Contact information
         username: Enter a username
       registrations:
         closed_message:
@@ -136,7 +135,6 @@ io:
           title: Closed registration message
         open:
           title: Open registration
-      setting: Setting
       site_description:
         desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
         title: Site description
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 5b91aa75d..d57fe8da2 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -155,7 +155,6 @@ ja:
     settings:
       contact_information:
         email: 公開するメールアドレスを入力
-        label: 連絡先情報
         username: ユーザー名を入力
       registrations:
         closed_message:
@@ -163,7 +162,6 @@ ja:
           title: 新規登録停止時のメッセージ
         open:
           title: 新規登録を受け付ける
-      setting: 設定
       site_description:
         desc_html: トップページへの表示と meta タグに使用されます。<br>HTMLタグ、特に<code>&lt;a&gt;</code> と <code>&lt;em&gt;</code>が利用可能です。
         title: サイトの説明文
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index c8ad38d41..bafc19993 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -155,7 +155,6 @@ ko:
     settings:
       contact_information:
         email: 공개할 메일 주소를 입력
-        label: 연락처 정보
         username: 아이디를 입력
       registrations:
         closed_message:
@@ -163,7 +162,6 @@ ko:
           title: 신규 등록 정지 시 메시지
         open:
           title: 신규 등록을 받음
-      setting: 설정
       site_description:
         desc_html: 탑 페이지와 meta 태그에 사용됩니다.<br>HTML 태그, 예를 들어<code>&lt;a&gt;</code> 태그와 <code>&lt;em&gt;</code> 태그를 사용할 수 있습니다.
         title: 사이트 설명
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 633061b06..dfc58f6b3 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -55,7 +55,6 @@ nl:
     settings:
       contact_information:
         email: Vul een openbaar gebruikt e-mailadres in
-        label: Contactgegevens
         username: Vul een gebruikersnaam in
       registrations:
         closed_message:
@@ -63,7 +62,6 @@ nl:
           title: Bericht wanneer registratie is uitgeschakeld
         open:
           title: Open registratie
-      setting: Instelling
       site_description:
         desc_html: Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.<br>Je kan HTML gebruiken, zoals <code>&lt;a&gt;</code> en <code>&lt;em&gt;</code>.
         title: Omschrijving Mastodon-server
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 05714959d..004e1ff80 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -142,7 +142,6 @@
     settings:
       contact_information:
         email: Skriv en offentlig e-postadresse
-        label: Kontaktinformasjon
         username: Skriv brukernavn
       registrations:
         closed_message:
@@ -150,7 +149,6 @@
           title: Melding for lukket registrering
         open:
           title: Åpen registrering
-      setting: Innstilling
       site_description:
         desc_html: Vises som et avsnitt på forsiden og brukes som en meta-tagg.<br> Du kan bruke HTML-tagger, spesielt <code>&lt;a&gt;</code> og <code>&lt;em&gt;</code>.
         title: Nettstedsbeskrivelse
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 30d5258a5..91a6ca791 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -149,7 +149,6 @@ oc:
     settings:
       contact_information:
         email: Picatz una adreça de corrièl
-        label: Informacions de contacte
         username: Picatz un nom d’utilizaire
       registrations:
         closed_message:
@@ -157,7 +156,6 @@ oc:
           title: Messatge de barradura de las inscripcions
         open:
           title: Inscripcions
-      setting: Paramètre
       site_description:
         desc_html: Afichada jos la forma de paragrafe sus la pagina d’acuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
         title: Descripcion del site
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 018ff3c7b..c6588e846 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -155,7 +155,6 @@ pl:
     settings:
       contact_information:
         email: Wprowadź publiczny adres e-mail
-        label: Informacje kontaktowe
         username: Wprowadź nazwę użytkownika
       registrations:
         closed_message:
@@ -163,7 +162,6 @@ pl:
           title: Wiadomość o nieaktywnej rejestracji
         open:
           title: Otwarta rejestracja
-      setting: Ustawienie
       site_description:
         desc_html: Wyświetlany jako nagłówek na stronie głównej oraz jako meta tag.<br>Możesz korzystać z tagów HTML, w szczególności z <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Opis strony
@@ -208,12 +206,12 @@ pl:
   authorize_follow:
     error: Niestety, podczas sprawdzania zdalnego konta wystąpił błąd
     follow: Śledź
-    following: 'Pomyślnie! Od teraz śledzisz:'
     follow_request: 'Wysłano prośbę o pozwolenie na obserwację:'
+    following: 'Pomyślnie! Od teraz śledzisz:'
     post_follow:
-      web: Przejdź do sieci
-      return: Powróć do strony użytkownika
       close: Ewentualnie, możesz po prostu zamknąć tą stronę.
+      return: Powróć do strony użytkownika
+      web: Przejdź do sieci
     prompt_html: 'Ty (<strong>%{self}</strong>) chcesz śledzić:'
     title: Śledź %{acct}
   datetime:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index fb5e03c8c..355c20d05 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -141,7 +141,6 @@ pt-BR:
     settings:
       contact_information:
         email: Entre um endereço de email público
-        label: Informação de contato
         username: Entre com usuário
       registrations:
         closed_message:
@@ -149,7 +148,6 @@ pt-BR:
           title: Mensagem de registro fechados
         open:
           title: Aberto para registro
-      setting: Preferências
       site_description:
         desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
         title: Descrição do site
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 0f59185a7..40be8a6c5 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -136,7 +136,6 @@ pt:
     settings:
       contact_information:
         email: Inserir um endereço de email para tornar público
-        label: Informação de contacto
         username: Insira um nome de utilizador
       registrations:
         closed_message:
@@ -144,7 +143,6 @@ pt:
           title: Mensagem de registos encerrados
         open:
           title: Aceitar novos registos
-      setting: Preferências
       site_description:
         desc_html: Mostrar como parágrafo na página inicial e usado como meta tag.<br/>Podes usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
         title: Descrição do site
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 414d39dd2..5cfc2b1ca 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -123,7 +123,6 @@ ru:
     settings:
       contact_information:
         email: Введите публичный e-mail
-        label: Контактная информация
         username: Введите имя пользователя
       registrations:
         closed_message:
@@ -131,7 +130,6 @@ ru:
           title: Сообщение о закрытой регистрации
         open:
           title: Открыть регистрацию
-      setting: Настройка
       site_description:
         desc_html: Отображается в качестве параграфа на титульной странице и используется в качестве мета-тега.<br>Можно использовать HTML-теги, в особенности <code>&lt;a&gt;</code> и <code>&lt;em&gt;</code>.
         title: Описание сайта
diff --git a/config/locales/th.yml b/config/locales/th.yml
index a71252afe..263babdd0 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -142,7 +142,6 @@ th:
     settings:
       contact_information:
         email: กรอกที่อยู่อีเมล์สาธารณะ
-        label: ข้อมูลที่ติดต่อ
         username: กรอกชื่อผู้ใช้
       registrations:
         closed_message:
@@ -150,7 +149,6 @@ th:
           title: ปิดข้อความลงทะเบียน
         open:
           title: เปิดรับลงทะเบียน
-      setting: ตั้งค่า
       site_description:
         desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br> ใช้ HTML tags ได้, in particular <code>&lt;a&gt;</code> และ <code>&lt;em&gt;</code>.
         title: คำอธิบายไซต์
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index a8927eaa8..e7864cc57 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -141,7 +141,6 @@ tr:
     settings:
       contact_information:
         email: Herkese açık e-posta adresiniz
-        label: İletişim bilgisi
         username: Bir kullanıcı adı giriniz
       registrations:
         closed_message:
@@ -149,7 +148,6 @@ tr:
           title: Kayıt alımları kapatılma mesajı
         open:
           title: Kayıt alımları
-      setting: Ayar adı
       site_description:
         desc_html: Ana sayfada paragraf olarak görüntülenecek bilgidir.<br>Özellikle <code>&lt;a&gt;</code> ve <code>&lt;em&gt;</code> olmak suretiyle HTML etiketlerini kullanabilirsiniz.
         title: Site açıklaması
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 65f1aabf5..129fc5bb7 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -123,7 +123,6 @@ uk:
     settings:
       contact_information:
         email: Введіть публічний email
-        label: Контактна інформація
         username: Введіть ім'я користувача
       registrations:
         closed_message:
@@ -131,7 +130,6 @@ uk:
           title: Повідомлення про закриту реєстрацію
         open:
           title: Відкрити реєстрацію
-      setting: Налаштування
       site_description:
         desc_html: Відображається у якості параграфа на титульній сторінці та використовується у якості мета-тега.<br>Можна використовувати HTML-теги, особливо <code>&lt;a&gt;</code> і <code>&lt;em&gt;</code>.
         title: Опис сайту
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index d5d1b672c..650d4bd15 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -148,7 +148,6 @@ zh-CN:
     settings:
       contact_information:
         email: 输入一个公开的电邮地址
-        label: 联系数据
         username: 输入用户名称
       registrations:
         closed_message:
@@ -156,7 +155,6 @@ zh-CN:
           title: 暂停注册消息
         open:
           title: 开放注册
-      setting: 设置
       site_description:
         desc_html: 在首页显示,及在 meta 标签中用作网站介绍。<br>你可以在此使用 HTML 标签,尤其是<code>&lt;a&gt;</code> 和 <code>&lt;em&gt;</code>。
         title: 本站介绍
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index aa9f27912..d2db78be1 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -141,7 +141,6 @@ zh-HK:
     settings:
       contact_information:
         email: 輸入一個公開的電郵地址
-        label: 聯絡資料
         username: 輸入用戶名稱
       registrations:
         closed_message:
@@ -149,7 +148,6 @@ zh-HK:
           title: 暫停註冊訊息
         open:
           title: 開放註冊
-      setting: 設定
       site_description:
         desc_html: 在首頁顯示,及在 meta 標籤使用作網站介紹。<br/> 你可以在此使用 <code>&lt;a&gt;</code> 和 <code>&lt;em&gt;</code> 等 HTML 標籤。
         title: 本站介紹
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 58caf7848..67aa2830f 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -102,7 +102,6 @@ zh-TW:
     settings:
       contact_information:
         email: 請輸入輸入一個公開電子信箱
-        label: 聯絡資訊
         username: 請輸入使用者名稱
       registrations:
         closed_message:
@@ -110,7 +109,6 @@ zh-TW:
           title: 關閉註冊訊息
         open:
           title: 開放註冊
-      setting: 設定
       site_description:
         desc_html: 顯示在首頁並且作為 meta 標籤的短文。<br>可使用 HTML 標籤,包括 <code>&lt;a&gt;</code> 及 <code>&lt;em&gt;</code>。
         title: 網站描述
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
index d9dde3c92..609bc762b 100644
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
         it 'cannot create a setting value for a non-admin key' do
           expect(Setting.new_setting_key).to be_blank
 
-          patch :update, params: { new_setting_key: 'New key value' }
+          patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
 
           expect(response).to redirect_to(edit_admin_settings_path)
           expect(Setting.new_setting_key).to be_nil
@@ -40,7 +40,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
         it 'creates a settings value that didnt exist before for eligible key' do
           expect(Setting.site_extended_description).to be_blank
 
-          patch :update, params: { site_extended_description: 'New key value' }
+          patch :update, params: { form_admin_settings: { site_extended_description: 'New key value' } }
 
           expect(response).to redirect_to(edit_admin_settings_path)
           expect(Setting.site_extended_description).to eq 'New key value'
@@ -56,7 +56,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
 
         it 'updates a settings value' do
           Setting.site_title = 'Original'
-          patch :update, params: { site_title: 'New title' }
+          patch :update, params: { form_admin_settings: { site_title: 'New title' } }
 
           expect(response).to redirect_to(edit_admin_settings_path)
           expect(Setting.site_title).to eq 'New title'
@@ -72,7 +72,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
 
         it 'typecasts open_registrations to boolean' do
           Setting.open_registrations = false
-          patch :update, params: { open_registrations: 'true' }
+          patch :update, params: { form_admin_settings: { open_registrations: '1' } }
 
           expect(response).to redirect_to(edit_admin_settings_path)
           expect(Setting.open_registrations).to eq true
-- 
cgit