about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/glitch/components/account/header.js2
-rw-r--r--app/javascript/glitch/components/local_settings/container.js4
-rw-r--r--app/javascript/glitch/components/local_settings/index.js2
-rw-r--r--app/javascript/glitch/components/local_settings/navigation/index.js2
-rw-r--r--app/javascript/glitch/components/local_settings/navigation/item/index.js2
-rw-r--r--app/javascript/glitch/components/local_settings/navigation/item/style.scss2
-rw-r--r--app/javascript/glitch/components/local_settings/navigation/style.scss2
-rw-r--r--app/javascript/glitch/components/local_settings/page/index.js2
-rw-r--r--app/javascript/glitch/components/local_settings/page/item/index.js2
-rw-r--r--app/javascript/glitch/components/local_settings/page/item/style.scss2
-rw-r--r--app/javascript/glitch/components/local_settings/page/style.scss2
-rw-r--r--app/javascript/glitch/components/local_settings/style.scss2
-rw-r--r--app/javascript/mastodon/base_polyfills.js2
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap35
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap26
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap114
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap23
-rw-r--r--app/javascript/mastodon/components/__tests__/avatar-test.js36
-rw-r--r--app/javascript/mastodon/components/__tests__/avatar_overlay-test.js29
-rw-r--r--app/javascript/mastodon/components/__tests__/button-test.js75
-rw-r--r--app/javascript/mastodon/components/__tests__/display_name-test.js18
-rw-r--r--app/javascript/mastodon/components/collapsable.js2
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js2
-rw-r--r--app/javascript/mastodon/components/icon_button.js60
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js3
-rw-r--r--app/javascript/mastodon/components/status_content.js3
-rw-r--r--app/javascript/mastodon/containers/mastodon.js1
-rw-r--r--app/javascript/mastodon/features/account/components/header.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js68
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/search.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_progress.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/warning.js2
-rw-r--r--app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js15
-rw-r--r--app/javascript/mastodon/features/compose/containers/sensitive_button_container.js2
-rw-r--r--app/javascript/mastodon/features/compose/index.js2
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji-test.js61
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js130
-rw-r--r--app/javascript/mastodon/features/emoji/emoji_compressed.js3
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js26
-rw-r--r--app/javascript/mastodon/features/ui/components/__tests__/column-test.js34
-rw-r--r--app/javascript/mastodon/features/ui/components/upload_area.js2
-rw-r--r--app/javascript/mastodon/features/ui/util/optional_motion.js56
-rw-r--r--app/javascript/mastodon/locales/ar.json7
-rw-r--r--app/javascript/mastodon/locales/bg.json7
-rw-r--r--app/javascript/mastodon/locales/ca.json12
-rw-r--r--app/javascript/mastodon/locales/de.json7
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json10
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/locales/eo.json7
-rw-r--r--app/javascript/mastodon/locales/es.json7
-rw-r--r--app/javascript/mastodon/locales/fa.json7
-rw-r--r--app/javascript/mastodon/locales/fi.json7
-rw-r--r--app/javascript/mastodon/locales/fr.json9
-rw-r--r--app/javascript/mastodon/locales/he.json7
-rw-r--r--app/javascript/mastodon/locales/hr.json7
-rw-r--r--app/javascript/mastodon/locales/hu.json7
-rw-r--r--app/javascript/mastodon/locales/id.json7
-rw-r--r--app/javascript/mastodon/locales/io.json7
-rw-r--r--app/javascript/mastodon/locales/it.json7
-rw-r--r--app/javascript/mastodon/locales/ja.json2
-rw-r--r--app/javascript/mastodon/locales/ko.json37
-rw-r--r--app/javascript/mastodon/locales/nl.json7
-rw-r--r--app/javascript/mastodon/locales/no.json7
-rw-r--r--app/javascript/mastodon/locales/oc.json7
-rw-r--r--app/javascript/mastodon/locales/pl.json7
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json15
-rw-r--r--app/javascript/mastodon/locales/pt.json7
-rw-r--r--app/javascript/mastodon/locales/ru.json7
-rw-r--r--app/javascript/mastodon/locales/sv.json217
-rw-r--r--app/javascript/mastodon/locales/th.json7
-rw-r--r--app/javascript/mastodon/locales/tr.json7
-rw-r--r--app/javascript/mastodon/locales/uk.json7
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json7
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json7
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json7
-rw-r--r--app/javascript/mastodon/main.js5
-rw-r--r--app/javascript/mastodon/middleware/sounds.js6
-rw-r--r--app/javascript/mastodon/reducers/timelines.js16
-rw-r--r--app/javascript/mastodon/test_setup.js5
-rw-r--r--app/javascript/packs/application.js3
-rw-r--r--app/javascript/packs/common.js3
-rw-r--r--app/javascript/styles/application.scss39
-rw-r--r--app/javascript/styles/mastodon/_mixins.scss (renamed from app/javascript/styles/_mixins.scss)0
-rw-r--r--app/javascript/styles/mastodon/about.scss (renamed from app/javascript/styles/about.scss)0
-rw-r--r--app/javascript/styles/mastodon/accounts.scss (renamed from app/javascript/styles/accounts.scss)0
-rw-r--r--app/javascript/styles/mastodon/admin.scss (renamed from app/javascript/styles/admin.scss)0
-rw-r--r--app/javascript/styles/mastodon/basics.scss (renamed from app/javascript/styles/basics.scss)2
-rw-r--r--app/javascript/styles/mastodon/boost.scss (renamed from app/javascript/styles/boost.scss)0
-rw-r--r--app/javascript/styles/mastodon/compact_header.scss (renamed from app/javascript/styles/compact_header.scss)0
-rw-r--r--app/javascript/styles/mastodon/components.scss (renamed from app/javascript/styles/components.scss)12
-rw-r--r--app/javascript/styles/mastodon/containers.scss (renamed from app/javascript/styles/containers.scss)0
-rw-r--r--app/javascript/styles/mastodon/emoji_picker.scss (renamed from app/javascript/styles/emoji_picker.scss)0
-rw-r--r--app/javascript/styles/mastodon/footer.scss (renamed from app/javascript/styles/footer.scss)0
-rw-r--r--app/javascript/styles/mastodon/forms.scss (renamed from app/javascript/styles/forms.scss)0
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss (renamed from app/javascript/styles/landing_strip.scss)0
-rw-r--r--app/javascript/styles/mastodon/lists.scss (renamed from app/javascript/styles/lists.scss)0
-rw-r--r--app/javascript/styles/mastodon/reset.scss (renamed from app/javascript/styles/reset.scss)0
-rw-r--r--app/javascript/styles/mastodon/rtl.scss (renamed from app/javascript/styles/rtl.scss)0
-rw-r--r--app/javascript/styles/mastodon/stream_entries.scss (renamed from app/javascript/styles/stream_entries.scss)0
-rw-r--r--app/javascript/styles/mastodon/tables.scss (renamed from app/javascript/styles/tables.scss)0
-rw-r--r--app/javascript/styles/mastodon/variables.scss (renamed from app/javascript/styles/variables.scss)0
-rw-r--r--app/javascript/themes/spin/pack.js4
104 files changed, 1227 insertions, 218 deletions
diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js
index 6359c1775..f4a413aa3 100644
--- a/app/javascript/glitch/components/account/header.js
+++ b/app/javascript/glitch/components/account/header.js
@@ -48,7 +48,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 //  Mastodon imports  //
-import emojify from 'mastodon/features/emoji/emoji';
+import emojify from '../../../mastodon/features/emoji/emoji';
 import IconButton from '../../../mastodon/components/icon_button';
 import Avatar from '../../../mastodon/components/avatar';
 
diff --git a/app/javascript/glitch/components/local_settings/container.js b/app/javascript/glitch/components/local_settings/container.js
index 6c202a4e7..4569db99f 100644
--- a/app/javascript/glitch/components/local_settings/container.js
+++ b/app/javascript/glitch/components/local_settings/container.js
@@ -2,10 +2,10 @@
 import { connect } from 'react-redux';
 
 //  Mastodon imports  //
-import { closeModal } from 'mastodon/actions/modal';
+import { closeModal } from '../../../mastodon/actions/modal';
 
 //  Our imports  //
-import { changeLocalSetting } from 'glitch/actions/local_settings';
+import { changeLocalSetting } from '../../../glitch/actions/local_settings';
 import LocalSettings from '.';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/glitch/components/local_settings/index.js b/app/javascript/glitch/components/local_settings/index.js
index 7f7b93de4..ef711229a 100644
--- a/app/javascript/glitch/components/local_settings/index.js
+++ b/app/javascript/glitch/components/local_settings/index.js
@@ -8,7 +8,7 @@ import LocalSettingsPage from './page';
 import LocalSettingsNavigation from './navigation';
 
 //  Stylesheet imports
-import './style';
+import './style.scss';
 
 export default class LocalSettings extends React.PureComponent {
 
diff --git a/app/javascript/glitch/components/local_settings/navigation/index.js b/app/javascript/glitch/components/local_settings/navigation/index.js
index 1f72cc824..fa35e83c7 100644
--- a/app/javascript/glitch/components/local_settings/navigation/index.js
+++ b/app/javascript/glitch/components/local_settings/navigation/index.js
@@ -7,7 +7,7 @@ import { injectIntl, defineMessages } from 'react-intl';
 import LocalSettingsNavigationItem from './item';
 
 //  Stylesheet imports
-import './style';
+import './style.scss';
 
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
diff --git a/app/javascript/glitch/components/local_settings/navigation/item/index.js b/app/javascript/glitch/components/local_settings/navigation/item/index.js
index 1676aa404..a352d5fb2 100644
--- a/app/javascript/glitch/components/local_settings/navigation/item/index.js
+++ b/app/javascript/glitch/components/local_settings/navigation/item/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 
 //  Stylesheet imports
-import './style';
+import './style.scss';
 
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
diff --git a/app/javascript/glitch/components/local_settings/navigation/item/style.scss b/app/javascript/glitch/components/local_settings/navigation/item/style.scss
index 33d7d3744..7f7371993 100644
--- a/app/javascript/glitch/components/local_settings/navigation/item/style.scss
+++ b/app/javascript/glitch/components/local_settings/navigation/item/style.scss
@@ -1,4 +1,4 @@
-@import 'styles/variables';
+@import 'styles/mastodon/variables';
 
 .glitch.local-settings__navigation__item {
   display: block;
diff --git a/app/javascript/glitch/components/local_settings/navigation/style.scss b/app/javascript/glitch/components/local_settings/navigation/style.scss
index a610a1212..0336f943b 100644
--- a/app/javascript/glitch/components/local_settings/navigation/style.scss
+++ b/app/javascript/glitch/components/local_settings/navigation/style.scss
@@ -1,4 +1,4 @@
-@import 'styles/variables';
+@import 'styles/mastodon/variables';
 
 .glitch.local-settings__navigation {
   background: $primary-text-color;
diff --git a/app/javascript/glitch/components/local_settings/page/index.js b/app/javascript/glitch/components/local_settings/page/index.js
index 338d86333..366c113c0 100644
--- a/app/javascript/glitch/components/local_settings/page/index.js
+++ b/app/javascript/glitch/components/local_settings/page/index.js
@@ -8,7 +8,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 import LocalSettingsPageItem from './item';
 
 //  Stylesheet imports
-import './style';
+import './style.scss';
 
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
diff --git a/app/javascript/glitch/components/local_settings/page/item/index.js b/app/javascript/glitch/components/local_settings/page/item/index.js
index 326c7eeb0..37e28c084 100644
--- a/app/javascript/glitch/components/local_settings/page/item/index.js
+++ b/app/javascript/glitch/components/local_settings/page/item/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
 //  Stylesheet imports
-import './style';
+import './style.scss';
 
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
diff --git a/app/javascript/glitch/components/local_settings/page/item/style.scss b/app/javascript/glitch/components/local_settings/page/item/style.scss
index da1941b99..b2d8f7185 100644
--- a/app/javascript/glitch/components/local_settings/page/item/style.scss
+++ b/app/javascript/glitch/components/local_settings/page/item/style.scss
@@ -1,4 +1,4 @@
-@import 'styles/variables';
+@import 'styles/mastodon/variables';
 
 .glitch.local-settings__page__item {
   select {
diff --git a/app/javascript/glitch/components/local_settings/page/style.scss b/app/javascript/glitch/components/local_settings/page/style.scss
index 53c95ea40..e9eedcad0 100644
--- a/app/javascript/glitch/components/local_settings/page/style.scss
+++ b/app/javascript/glitch/components/local_settings/page/style.scss
@@ -1,4 +1,4 @@
-@import 'styles/variables';
+@import 'styles/mastodon/variables';
 
 .glitch.local-settings__page {
   display: block;
diff --git a/app/javascript/glitch/components/local_settings/style.scss b/app/javascript/glitch/components/local_settings/style.scss
index 54fec47bd..765294607 100644
--- a/app/javascript/glitch/components/local_settings/style.scss
+++ b/app/javascript/glitch/components/local_settings/style.scss
@@ -1,4 +1,4 @@
-@import 'styles/variables';
+@import 'styles/mastodon/variables';
 
 .glitch.local-settings {
   position: relative;
diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js
index 266a0020c..7856b26f9 100644
--- a/app/javascript/mastodon/base_polyfills.js
+++ b/app/javascript/mastodon/base_polyfills.js
@@ -1,5 +1,5 @@
 import 'intl';
-import 'intl/locale-data/jsonp/en.js';
+import 'intl/locale-data/jsonp/en';
 import 'es6-symbol/implement';
 import includes from 'array-includes';
 import assign from 'object-assign';
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
new file mode 100644
index 000000000..4005c860f
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
+<div
+  className="account__avatar"
+  data-avatar-of="@alice"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  style={
+    Object {
+      "backgroundImage": "url(/animated/alice.gif)",
+      "backgroundSize": "100px 100px",
+      "height": "100px",
+      "width": "100px",
+    }
+  }
+/>
+`;
+
+exports[`<Avatar /> Still renders a still avatar 1`] = `
+<div
+  className="account__avatar"
+  data-avatar-of="@alice"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  style={
+    Object {
+      "backgroundImage": "url(/static/alice.jpg)",
+      "backgroundSize": "100px 100px",
+      "height": "100px",
+      "width": "100px",
+    }
+  }
+/>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
new file mode 100644
index 000000000..d9e5e5252
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<AvatarOverlay renders a overlay avatar 1`] = `
+<div
+  className="account__avatar-overlay"
+>
+  <div
+    className="account__avatar-overlay-base"
+    data-avatar-of="@alice"
+    style={
+      Object {
+        "backgroundImage": "url(/static/alice.jpg)",
+      }
+    }
+  />
+  <div
+    className="account__avatar-overlay-overlay"
+    data-avatar-of="@eve@blackhat.lair"
+    style={
+      Object {
+        "backgroundImage": "url(/static/eve.jpg)",
+      }
+    }
+  />
+</div>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
new file mode 100644
index 000000000..c3f018d90
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
@@ -0,0 +1,114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
+<button
+  className="button button-secondary"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders a button element 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
+<button
+  className="button"
+  disabled={true}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders class="button--block" if props.block given 1`] = `
+<button
+  className="button button--block"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders the children 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  <p>
+    children
+  </p>
+</button>
+`;
+
+exports[`<Button /> renders the given text 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  foo
+</button>
+`;
+
+exports[`<Button /> renders the props.text instead of children 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  foo
+</button>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
new file mode 100644
index 000000000..533359ffe
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<DisplayName /> renders display name + account name 1`] = `
+<span
+  className="display-name"
+>
+  <strong
+    className="display-name__html"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>Foo</p>",
+      }
+    }
+  />
+   
+  <span
+    className="display-name__account"
+  >
+    @
+    bar@baz
+  </span>
+</span>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/avatar-test.js b/app/javascript/mastodon/components/__tests__/avatar-test.js
new file mode 100644
index 000000000..dd3f7b7d2
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/avatar-test.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS } from 'immutable';
+import Avatar from '../avatar';
+
+describe('<Avatar />', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+
+  const size     = 100;
+
+  describe('Autoplay', () => {
+    it('renders a animated avatar', () => {
+      const component = renderer.create(<Avatar account={account} animate size={size} />);
+      const tree      = component.toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  describe('Still', () => {
+    it('renders a still avatar', () => {
+      const component = renderer.create(<Avatar account={account} size={size} />);
+      const tree      = component.toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  // TODO add autoplay test if possible
+});
diff --git a/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js b/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
new file mode 100644
index 000000000..44addea83
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS } from 'immutable';
+import AvatarOverlay from '../avatar_overlay';
+
+describe('<AvatarOverlay', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+
+  const friend = fromJS({
+    username: 'eve',
+    acct: 'eve@blackhat.lair',
+    display_name: 'Evelyn',
+    avatar: '/animated/eve.gif',
+    avatar_static: '/static/eve.jpg',
+  });
+
+  it('renders a overlay avatar', () => {
+    const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/__tests__/button-test.js b/app/javascript/mastodon/components/__tests__/button-test.js
new file mode 100644
index 000000000..160cd3cbc
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/button-test.js
@@ -0,0 +1,75 @@
+import { shallow } from 'enzyme';
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Button from '../button';
+
+describe('<Button />', () => {
+  it('renders a button element', () => {
+    const component = renderer.create(<Button />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the given text', () => {
+    const text      = 'foo';
+    const component = renderer.create(<Button text={text} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('handles click events using the given handler', () => {
+    const handler = jest.fn();
+    const button  = shallow(<Button onClick={handler} />);
+    button.find('button').simulate('click');
+
+    expect(handler.mock.calls.length).toEqual(1);
+  });
+
+  it('does not handle click events if props.disabled given', () => {
+    const handler = jest.fn();
+    const button  = shallow(<Button onClick={handler} disabled />);
+    button.find('button').simulate('click');
+
+    expect(handler.mock.calls.length).toEqual(0);
+  });
+
+  it('renders a disabled attribute if props.disabled given', () => {
+    const component = renderer.create(<Button disabled />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the children', () => {
+    const children  = <p>children</p>;
+    const component = renderer.create(<Button>{children}</Button>);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the props.text instead of children', () => {
+    const text      = 'foo';
+    const children  = <p>children</p>;
+    const component = renderer.create(<Button text={text}>{children}</Button>);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders class="button--block" if props.block given', () => {
+    const component = renderer.create(<Button block />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('adds class "button-secondary" if props.secondary given', () => {
+    const component = renderer.create(<Button secondary />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/__tests__/display_name-test.js b/app/javascript/mastodon/components/__tests__/display_name-test.js
new file mode 100644
index 000000000..0d040c4cd
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/display_name-test.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS }  from 'immutable';
+import DisplayName from '../display_name';
+
+describe('<DisplayName />', () => {
+  it('renders display name + account name', () => {
+    const account = fromJS({
+      username: 'bar',
+      acct: 'bar@baz',
+      display_name_html: '<p>Foo</p>',
+    });
+    const component = renderer.create(<DisplayName account={account} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/collapsable.js b/app/javascript/mastodon/components/collapsable.js
index ad1453589..42ea37ec2 100644
--- a/app/javascript/mastodon/components/collapsable.js
+++ b/app/javascript/mastodon/components/collapsable.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 73ad46bb7..3a3ebf487 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import IconButton from './icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index 6fb191c6b..76b0da12f 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -1,7 +1,8 @@
 import React from 'react';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
+import classNames from 'classnames';
 
 export default class IconButton extends React.PureComponent {
 
@@ -56,27 +57,26 @@ export default class IconButton extends React.PureComponent {
       style.textAlign = 'left';
     }
 
-    const classes = ['icon-button'];
-
-    if (this.props.active) {
-      classes.push('active');
-    }
-
-    if (this.props.disabled) {
-      classes.push('disabled');
-    }
-
-    if (this.props.inverted) {
-      classes.push('inverted');
-    }
-
-    if (this.props.overlay) {
-      classes.push('overlayed');
-    }
-
-    if (this.props.className) {
-      classes.push(this.props.className);
-    }
+    const {
+      active,
+      animate,
+      className,
+      disabled,
+      expanded,
+      icon,
+      inverted,
+      overlay,
+      pressed,
+      tabIndex,
+      title,
+    } = this.props;
+
+    const classes = classNames(className, 'icon-button', {
+      active,
+      disabled,
+      inverted,
+      overlayed: overlay,
+    });
 
     const flipDeg = this.props.flip ? -180 : -360;
     const rotateDeg = this.props.active ? flipDeg : 0;
@@ -90,23 +90,23 @@ export default class IconButton extends React.PureComponent {
       damping: 7,
     };
     const motionStyle = {
-      rotate: this.props.animate ? spring(rotateDeg, springOpts) : 0,
+      rotate: animate ? spring(rotateDeg, springOpts) : 0,
     };
 
     return (
       <Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
         {({ rotate }) =>
           <button
-            aria-label={this.props.title}
-            aria-pressed={this.props.pressed}
-            aria-expanded={this.props.expanded}
-            title={this.props.title}
-            className={classes.join(' ')}
+            aria-label={title}
+            aria-pressed={pressed}
+            aria-expanded={expanded}
+            title={title}
+            className={classes}
             onClick={this.handleClick}
             style={style}
-            tabIndex={this.props.tabIndex}
+            tabIndex={tabIndex}
           >
-            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
+            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
             {this.props.label}
           </button>
         }
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index cf9c8fb53..af152cc32 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -16,6 +16,7 @@ const messages = defineMessages({
   block: { id: 'account.block', defaultMessage: 'Block @{name}' },
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
   share: { id: 'status.share', defaultMessage: 'Share' },
+  more: { id: 'status.more', defaultMessage: 'More' },
   replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
   reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
   cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
@@ -182,7 +183,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
         {shareButton}
 
         <div className='status__action-bar-dropdown'>
-          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
+          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 8ad60b9d6..0f7f15dfc 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -125,6 +125,7 @@ export default class StatusContent extends React.PureComponent {
     const directionStyle = { direction: 'ltr' };
     const classNames = classnames('status__content', {
       'status__content--with-action': this.props.onClick && this.context.router,
+      'status__content--with-spoiler': status.get('spoiler_text').length > 0,
     });
 
     if (isRtl(status.get('search_index'))) {
@@ -156,7 +157,7 @@ export default class StatusContent extends React.PureComponent {
 
           {mentionsPlaceholder}
 
-          <div tabIndex={!hidden && 0} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
+          <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
         </div>
       );
     } else if (this.props.onClick) {
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 6beffca1c..a7138e62d 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -10,6 +10,7 @@ import { hydrateStore } from '../actions/store';
 import { connectUserStream } from '../actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
+
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 5402d6753..57678d162 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import IconButton from '../../../components/icon_button';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { connect } from 'react-redux';
 import ImmutablePureComponent from 'react-immutable-pure-component';
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 5b06cef7c..17f5bde4b 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -58,7 +58,6 @@ export default class ComposeForm extends ImmutablePureComponent {
     onPickEmoji: PropTypes.func.isRequired,
     showSearch: PropTypes.bool,
     settings : ImmutablePropTypes.map.isRequired,
-    filesAttached : PropTypes.bool,
   };
 
   static defaultProps = {
@@ -156,13 +155,12 @@ export default class ComposeForm extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, onPaste, showSearch, filesAttached } = this.props;
+    const { intl, onPaste, showSearch } = this.props;
     const disabled = this.props.is_submitting;
     const maybeEye = (this.props.advanced_options && this.props.advanced_options.do_not_federate) ? ' 👁️' : '';
     const text     = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join('');
 
     const secondaryVisibility = this.props.settings.get('side_arm');
-    const isWideView = this.props.settings.get('stretch');
     let showSideArm = secondaryVisibility !== 'none';
 
     let publishText = '';
@@ -182,14 +180,9 @@ export default class ComposeForm extends ImmutablePureComponent {
           {
             <i
               className={`fa fa-${privacyIcons[this.props.privacy]}`}
-              style={{
-                paddingRight: (filesAttached || !isWideView) ? '0' : '5px',
-              }}
+              style={{ paddingRight: '5px' }}
             />
-          }{
-            (filesAttached || !isWideView) ? '' :
-              intl.formatMessage(messages.publish)
-          }
+          }{intl.formatMessage(messages.publish)}
         </span>
       );
 
@@ -247,36 +240,33 @@ export default class ComposeForm extends ImmutablePureComponent {
           <UploadFormContainer />
         </div>
 
-        <div className='compose-form__buttons-wrapper'>
-          <div className='compose-form__buttons'>
-            <UploadButtonContainer />
-            <DoodleButtonContainer />
-            <PrivacyDropdownContainer />
-            <ComposeAdvancedOptionsContainer />
-            <SensitiveButtonContainer />
-            <SpoilerButtonContainer />
-          </div>
+        <div className='compose-form__buttons'>
+          <UploadButtonContainer />
+          <DoodleButtonContainer />
+          <PrivacyDropdownContainer />
+          <ComposeAdvancedOptionsContainer />
+          <SensitiveButtonContainer />
+          <SpoilerButtonContainer />
+        </div>
 
-          <div className='compose-form__publish'>
-            <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
-            <div className='compose-form__publish-button-wrapper'>
-              {
-                showSideArm ?
-                  <Button
-                    className='compose-form__publish__side-arm'
-                    text={publishText2}
-                    onClick={this.handleSubmit2}
-                    disabled={submitDisabled}
-                  /> : ''
-              }
-              <Button
-                className='compose-form__publish__primary'
-                text={publishText}
-                onClick={this.handleSubmit}
-                disabled={submitDisabled}
-                block
-              />
-            </div>
+        <div className='compose-form__publish'>
+          <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
+          <div className='compose-form__publish-button-wrapper'>
+            {
+              showSideArm ?
+                <Button
+                  className='compose-form__publish__side-arm'
+                  text={publishText2}
+                  onClick={this.handleSubmit2}
+                  disabled={submitDisabled}
+                /> : ''
+            }
+            <Button
+              className='compose-form__publish__primary'
+              text={publishText}
+              onClick={this.handleSubmit}
+              disabled={submitDisabled}
+            />
           </div>
         </div>
       </div>
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index e38ed38c1..c1e85aee3 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { injectIntl, defineMessages } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 import classNames from 'classnames';
diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js
index f57d54618..398fc44ce 100644
--- a/app/javascript/mastodon/features/compose/components/search.js
+++ b/app/javascript/mastodon/features/compose/components/search.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 const messages = defineMessages({
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index cd9e08360..5d8d66cf7 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -2,7 +2,7 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import IconButton from '../../../components/icon_button';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { defineMessages, injectIntl } from 'react-intl';
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js
index 3e49098c7..d5e6f19cd 100644
--- a/app/javascript/mastodon/features/compose/components/upload_progress.js
+++ b/app/javascript/mastodon/features/compose/components/upload_progress.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/mastodon/features/compose/components/warning.js b/app/javascript/mastodon/features/compose/components/warning.js
index a0814e984..803b7f86a 100644
--- a/app/javascript/mastodon/features/compose/components/warning.js
+++ b/app/javascript/mastodon/features/compose/components/warning.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 export default class Warning extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js b/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js
deleted file mode 100644
index a9e3a9edf..000000000
--- a/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { connect } from 'react-redux';
-import AutosuggestStatus from '../components/autosuggest_status';
-import { makeGetStatus } from '../../../selectors';
-
-const makeMapStateToProps = () => {
-  const getStatus = makeGetStatus();
-
-  const mapStateToProps = (state, { id }) => ({
-    status: getStatus(state, id),
-  });
-
-  return mapStateToProps;
-};
-
-export default connect(makeMapStateToProps)(AutosuggestStatus);
diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
index 8624849f3..e4bd5a743 100644
--- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import IconButton from '../../../components/icon_button';
 import { changeComposeSensitivity } from '../../../actions/compose';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { injectIntl, defineMessages } from 'react-intl';
 
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 9068648bd..41a97d550 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -10,7 +10,7 @@ import { changeLocalSetting } from '../../../glitch/actions/local_settings';
 import { Link } from 'react-router-dom';
 import { injectIntl, defineMessages } from 'react-intl';
 import SearchContainer from './containers/search_container';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import SearchResultsContainer from './containers/search_results_container';
 import { changeComposing } from '../../actions/compose';
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
new file mode 100644
index 000000000..636402172
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -0,0 +1,61 @@
+import emojify from '../emoji';
+
+describe('emoji', () => {
+  describe('.emojify', () => {
+    it('ignores unknown shortcodes', () => {
+      expect(emojify(':foobarbazfake:')).toEqual(':foobarbazfake:');
+    });
+
+    it('ignores shortcodes inside of tags', () => {
+      expect(emojify('<p data-foo=":smile:"></p>')).toEqual('<p data-foo=":smile:"></p>');
+    });
+
+    it('works with unclosed tags', () => {
+      expect(emojify('hello>')).toEqual('hello>');
+      expect(emojify('<hello')).toEqual('<hello');
+    });
+
+    it('works with unclosed shortcodes', () => {
+      expect(emojify('smile:')).toEqual('smile:');
+      expect(emojify(':smile')).toEqual(':smile');
+    });
+
+    it('does unicode', () => {
+      expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
+        '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
+      expect(emojify('👨‍👩‍👧‍👧')).toEqual(
+      '<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
+      expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
+      expect(emojify('\u2757')).toEqual(
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+    });
+
+    it('does multiple unicode', () => {
+      expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
+      expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
+      expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+      expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
+        'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
+    });
+
+    it('ignores unicode inside of tags', () => {
+      expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).toEqual('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
+    });
+
+    it('does multiple emoji properly (issue 5188)', () => {
+      expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
+      expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
+    });
+
+    it('does an emoji that has no shortcode', () => {
+      expect(emojify('🕉️')).toEqual('<img draggable="false" class="emojione" alt="🕉️" title="" src="/emoji/1f549.svg" />');
+    });
+
+    it('does an emoji whose filename is irregular', () => {
+      expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
+    });
+  });
+});
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
new file mode 100644
index 000000000..53efa5743
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
@@ -0,0 +1,130 @@
+import { pick } from 'lodash';
+import { emojiIndex } from 'emoji-mart';
+import { search } from '../emoji_mart_search_light';
+
+const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
+
+describe('emoji_index', () => {
+  it('should give same result for emoji_index_light and emoji-mart', () => {
+    const expected = [
+      {
+        id: 'pineapple',
+        unified: '1f34d',
+        native: '🍍',
+      },
+    ];
+    expect(search('pineapple').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('orders search results correctly', () => {
+    const expected = [
+      {
+        id: 'apple',
+        unified: '1f34e',
+        native: '🍎',
+      },
+      {
+        id: 'pineapple',
+        unified: '1f34d',
+        native: '🍍',
+      },
+      {
+        id: 'green_apple',
+        unified: '1f34f',
+        native: '🍏',
+      },
+      {
+        id: 'iphone',
+        unified: '1f4f1',
+        native: '📱',
+      },
+    ];
+    expect(search('apple').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('handles custom emoji', () => {
+    const custom = [
+      {
+        id: 'mastodon',
+        name: 'mastodon',
+        short_names: ['mastodon'],
+        text: '',
+        emoticons: [],
+        keywords: ['mastodon'],
+        imageUrl: 'http://example.com',
+        custom: true,
+      },
+    ];
+    search('', { custom });
+    emojiIndex.search('', { custom });
+    const expected = [
+      {
+        id: 'mastodon',
+        custom: true,
+      },
+    ];
+    expect(search('masto').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('should filter only emojis we care about, exclude pineapple', () => {
+    const emojisToShowFilter = unified => unified !== '1F34D';
+    expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
+      .not.toContain('pineapple');
+    expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
+      .not.toContain('pineapple');
+  });
+
+  it('can include/exclude categories', () => {
+    expect(search('flag', { include: ['people'] })).toEqual([]);
+    expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
+  });
+
+  it('does an emoji whose unified name is irregular', () => {
+    const expected = [
+      {
+        'id': 'water_polo',
+        'unified': '1f93d',
+        'native': '🤽',
+      },
+      {
+        'id': 'man-playing-water-polo',
+        'unified': '1f93d-200d-2642-fe0f',
+        'native': '🤽‍♂️',
+      },
+      {
+        'id': 'woman-playing-water-polo',
+        'unified': '1f93d-200d-2640-fe0f',
+        'native': '🤽‍♀️',
+      },
+    ];
+    expect(search('polo').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('can search for thinking_face', () => {
+    const expected = [
+      {
+        id: 'thinking_face',
+        unified: '1f914',
+        native: '🤔',
+      },
+    ];
+    expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('can search for woman-facepalming', () => {
+    const expected = [
+      {
+        id: 'woman-facepalming',
+        unified: '1f926-200d-2640-fe0f',
+        native: '🤦‍♀️',
+      },
+    ];
+    expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
+  });
+});
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js
index 3bd89cf3b..c0cba952a 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.js
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js
@@ -9,7 +9,8 @@ const { unicodeToFilename } = require('./unicode_to_filename');
 const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
 const emojiMap         = require('./emoji_map.json');
 const { emojiIndex } = require('emoji-mart');
-const emojiMartData = require('emoji-mart/dist/data').default;
+const { default: emojiMartData } = require('emoji-mart/dist/data');
+
 const excluded       = ['®', '©', '™'];
 const skins          = ['🏻', '🏼', '🏽', '🏾', '🏿'];
 const shortcodeMap   = {};
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 816f83e45..d8547db36 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -48,6 +48,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
     let media           = '';
     let mediaIcon       = null;
     let applicationLink = '';
+    let reblogLink = '';
+    let reblogIcon = 'retweet';
 
     if (status.get('media_attachments').size > 0) {
       if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
@@ -85,6 +87,23 @@ export default class DetailedStatus extends ImmutablePureComponent {
       applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
     }
 
+    if (status.get('visibility') === 'direct') {
+      reblogIcon = 'envelope';
+    } else if (status.get('visibility') === 'private') {
+      reblogIcon = 'lock';
+    }
+
+    if (status.get('visibility') === 'private') {
+      reblogLink = <i className={`fa fa-${reblogIcon}`} />;
+    } else {
+      reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
+        <i className={`fa fa-${reblogIcon}`} />
+        <span className='detailed-status__reblogs'>
+          <FormattedNumber value={status.get('reblogs_count')} />
+        </span>
+      </Link>);
+    }
+
     return (
       <div className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
@@ -101,12 +120,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
         <div className='detailed-status__meta'>
           <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
             <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-          </a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
-            <i className='fa fa-retweet' />
-            <span className='detailed-status__reblogs'>
-              <FormattedNumber value={status.get('reblogs_count')} />
-            </span>
-          </Link> · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
+          </a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
             <i className='fa fa-star' />
             <span className='detailed-status__favorites'>
               <FormattedNumber value={status.get('favourites_count')} />
diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
new file mode 100644
index 000000000..1e5e1d8dc
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import Column from '../column';
+import ColumnHeader from '../column_header';
+
+describe('<Column />', () => {
+  describe('<ColumnHeader /> click handler', () => {
+    const originalRaf = global.requestAnimationFrame;
+
+    beforeEach(() => {
+      global.requestAnimationFrame = jest.fn();
+    });
+
+    afterAll(() => {
+      global.requestAnimationFrame = originalRaf;
+    });
+
+    it('runs the scroll animation if the column contains scrollable content', () => {
+      const wrapper = mount(
+        <Column heading='notifications'>
+          <div className='scrollable' />
+        </Column>
+      );
+      wrapper.find(ColumnHeader).simulate('click');
+      expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
+    });
+
+    it('does not try to scroll if there is no scrollable content', () => {
+      const wrapper = mount(<Column heading='notifications' />);
+      wrapper.find(ColumnHeader).simulate('click');
+      expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
+    });
+  });
+});
diff --git a/app/javascript/mastodon/features/ui/components/upload_area.js b/app/javascript/mastodon/features/ui/components/upload_area.js
index dda28feeb..c19065be6 100644
--- a/app/javascript/mastodon/features/ui/components/upload_area.js
+++ b/app/javascript/mastodon/features/ui/components/upload_area.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js
new file mode 100644
index 000000000..af6368738
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/optional_motion.js
@@ -0,0 +1,56 @@
+// Like react-motion's Motion, but checks to see if the user prefers
+// reduced motion and uses a cross-fade in those cases.
+
+import React from 'react';
+import Motion from 'react-motion/lib/Motion';
+import PropTypes from 'prop-types';
+
+const stylesToKeep = ['opacity', 'backgroundOpacity'];
+
+let reduceMotion;
+
+const extractValue = (value) => {
+  // This is either an object with a "val" property or it's a number
+  return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
+};
+
+class OptionalMotion extends React.Component {
+
+  static propTypes = {
+    defaultStyle: PropTypes.object,
+    style: PropTypes.object,
+    children: PropTypes.func,
+  }
+
+  render() {
+
+    const { style, defaultStyle, children } = this.props;
+
+    if (typeof reduceMotion !== 'boolean') {
+      // This never changes without a page reload, so we can just grab it
+      // once from the body classes as opposed to using Redux's connect(),
+      // which would unnecessarily update every state change
+      reduceMotion = document.body.classList.contains('reduce-motion');
+    }
+    if (reduceMotion) {
+      Object.keys(style).forEach(key => {
+        if (stylesToKeep.includes(key)) {
+          return;
+        }
+        // If it's setting an x or height or scale or some other value, we need
+        // to preserve the end-state value without actually animating it
+        style[key] = defaultStyle[key] = extractValue(style[key]);
+      });
+    }
+
+    return (
+      <Motion style={style} defaultStyle={defaultStyle}>
+        {children}
+      </Motion>
+    );
+  }
+
+}
+
+
+export default OptionalMotion;
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 799819c7c..7cc8ea237 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -82,7 +82,6 @@
   "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",
   "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
   "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
-  "empty_column.home.inactivity": "صفحتك فارغة. لقد كنت غائبا لفترة طويلة عن الشبكة. سوف تملأ تلقائيا في وقت قريب.",
   "empty_column.home.public_timeline": "الخيط العام",
   "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
   "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "للعامة",
   "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
   "privacy.unlisted.short": "غير مدرج",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "إلغاء",
   "report.placeholder": "تعليقات إضافية",
   "report.submit": "إرسال",
@@ -179,6 +183,7 @@
   "status.load_more": "حمّل المزيد",
   "status.media_hidden": "الصورة مستترة",
   "status.mention": "أذكُر @{name}",
+  "status.more": "More",
   "status.mute_conversation": "كتم المحادثة",
   "status.open": "وسع هذه المشاركة",
   "status.pin": "تدبيس على الملف الشخصي",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index eeded31b7..da2372cff 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -82,7 +82,6 @@
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Отказ",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
@@ -179,6 +183,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Споменаване",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index fe2433591..af732921d 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -82,7 +82,6 @@
   "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",
   "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
   "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
-  "empty_column.home.inactivity": "La línia Inici és buida. si has estat inactiu durant un temps es regenerarà aviat.",
   "empty_column.home.public_timeline": "la línia de temps pública",
   "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
   "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
@@ -160,11 +159,11 @@
   "privacy.public.short": "Públic",
   "privacy.unlisted.long": "No publicar en línies de temps públiques",
   "privacy.unlisted.short": "No llistat",
-  "relative_time.days": "{number}d",
-  "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
-  "relative_time.minutes": "{number}m",
-  "relative_time.seconds": "{number}s",
+  "relative_time.days": "fa {number} jorns",
+  "relative_time.hours": "fa {number} hores",
+  "relative_time.just_now": "ara",
+  "relative_time.minutes": "fa {number} minutes",
+  "relative_time.seconds": "fa {number} segondes",
   "reply_indicator.cancel": "Cancel·lar",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Enviar",
@@ -184,6 +183,7 @@
   "status.load_more": "Carrega més",
   "status.media_hidden": "Multimèdia amagat",
   "status.mention": "Esmentar @{name}",
+  "status.more": "Més",
   "status.mute_conversation": "Silenciar conversació",
   "status.open": "Ampliar aquest estat",
   "status.pin": "Fixat en el perfil",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 9d9853236..283a2946f 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
   "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
   "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
-  "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv warst, wird sie für dich so schnell wie möglich neu erstellt.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
   "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Öffentlich",
   "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
   "privacy.unlisted.short": "Nicht gelistet",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Abbrechen",
   "report.placeholder": "Zusätzliche Kommentare",
   "report.submit": "Absenden",
@@ -179,6 +183,7 @@
   "status.load_more": "Weitere laden",
   "status.media_hidden": "Medien versteckt",
   "status.mention": "@{name} erwähnen",
+  "status.more": "Mehr",
   "status.mute_conversation": "Thread stummschalten",
   "status.open": "Diesen Beitrag öffnen",
   "status.pin": "Im Profil anheften",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 99ff3b35b..f400b283f 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -184,6 +184,10 @@
         "id": "status.share"
       },
       {
+        "defaultMessage": "More",
+        "id": "status.more"
+      },
+      {
         "defaultMessage": "Reply to thread",
         "id": "status.replyAll"
       },
@@ -908,10 +912,6 @@
         "id": "column.home"
       },
       {
-        "defaultMessage": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
-        "id": "empty_column.home.inactivity"
-      },
-      {
         "defaultMessage": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
         "id": "empty_column.home"
       },
@@ -1408,4 +1408,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
+]
\ No newline at end of file
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 12efe0e0c..1d0bbcee5 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -82,7 +82,6 @@
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not post to public timelines",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancel",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
@@ -179,6 +183,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mention @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 8f90bdf78..22639f6f9 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -82,7 +82,6 @@
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Rezigni",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
@@ -179,6 +183,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mencii @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index f6bfbb04d..6e8e94700 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -82,7 +82,6 @@
   "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
   "empty_column.hashtag": "No hay nada en este hashtag aún.",
   "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
-  "empty_column.home.inactivity": "Tus notificaciones están vacías. Si has estado inactivo por un tiempo, se regenerará para ti pronto.",
   "empty_column.home.public_timeline": "la línea de tiempo pública",
   "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
   "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Público",
   "privacy.unlisted.long": "No mostrar en la historia federada",
   "privacy.unlisted.short": "Sin federar",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "ahora",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
   "report.placeholder": "Comentarios adicionales",
   "report.submit": "Publicar",
@@ -179,6 +183,7 @@
   "status.load_more": "Cargar más",
   "status.media_hidden": "Contenido multimedia oculto",
   "status.mention": "Mencionar",
+  "status.more": "Más",
   "status.mute_conversation": "Silenciar conversación",
   "status.open": "Expandir estado",
   "status.pin": "Fijar",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 9df0dec42..995d1b5ae 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -82,7 +82,6 @@
   "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",
   "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
   "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
-  "empty_column.home.inactivity": "فهرست پی‌گیری‌های شما خالی است. اگر مدتی است که غیرفعال بودید، این فهرست به زودی برایتان پر می‌شود.",
   "empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا",
   "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
   "empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود",
@@ -160,6 +159,11 @@
   "privacy.public.short": "عمومی",
   "privacy.unlisted.long": "عمومی، ولی فهرست نکن",
   "privacy.unlisted.short": "فهرست‌نشده",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "لغو",
   "report.placeholder": "توضیح اضافه",
   "report.submit": "بفرست",
@@ -179,6 +183,7 @@
   "status.load_more": "بیشتر نشان بده",
   "status.media_hidden": "تصویر پنهان شده",
   "status.mention": "نام‌بردن از @{name}",
+  "status.more": "More",
   "status.mute_conversation": "بی‌صداکردن گفتگو",
   "status.open": "این نوشته را باز کن",
   "status.pin": "نوشتهٔ ثابت نمایه",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 0f6554595..af08be5d1 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -82,7 +82,6 @@
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Peruuta",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
@@ -179,6 +183,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mainitse @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 145b683f3..219bf4da1 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -63,7 +63,7 @@
   "confirmations.mute.message": "Confirmez-vous le masquage de {name} ?",
   "confirmations.unfollow.confirm": "Ne plus suivre",
   "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?",
-  "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
+  "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
   "embed.preview": "Il apparaîtra comme cela : ",
   "emoji_button.activity": "Activités",
   "emoji_button.custom": "Personnalisés",
@@ -82,7 +82,6 @@
   "empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir !",
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
-  "empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
   "empty_column.home.public_timeline": "le fil public",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Ne pas afficher dans les fils publics",
   "privacy.unlisted.short": "Non-listé",
+  "relative_time.days": "{number} j",
+  "relative_time.hours": "{number} h",
+  "relative_time.just_now": "à l’instant",
+  "relative_time.minutes": "{number} min",
+  "relative_time.seconds": "{number} s",
   "reply_indicator.cancel": "Annuler",
   "report.placeholder": "Commentaires additionnels",
   "report.submit": "Envoyer",
@@ -179,6 +183,7 @@
   "status.load_more": "Charger plus",
   "status.media_hidden": "Média caché",
   "status.mention": "Mentionner",
+  "status.more": "Plus",
   "status.mute_conversation": "Masquer la conversation",
   "status.open": "Déplier ce statut",
   "status.pin": "Épingler sur le profil",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index beaa4fd3a..a260f0968 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -82,7 +82,6 @@
   "empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
   "empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.",
   "empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.",
-  "empty_column.home.inactivity": "ציר זמן הבית שלך ריק. אם לא הייתה פעילות מצידך לאחרונה, הוא יתמלא מחדש בקרוב.",
   "empty_column.home.public_timeline": "ציר זמן בין-קהילתי",
   "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!",
   "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "פומבי",
   "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים",
   "privacy.unlisted.short": "לא לפיד הכללי",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "ביטול",
   "report.placeholder": "הערות נוספות",
   "report.submit": "שליחה",
@@ -179,6 +183,7 @@
   "status.load_more": "עוד",
   "status.media_hidden": "מדיה מוסתרת",
   "status.mention": "פניה אל @{name}",
+  "status.more": "More",
   "status.mute_conversation": "השתקת שיחה",
   "status.open": "הרחבת הודעה",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index cef61f15e..6ac7fc3b4 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
   "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
   "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
-  "empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, uskoro ćese regenerirati.",
   "empty_column.home.public_timeline": "javni timeline",
   "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
   "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Javno",
   "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Otkaži",
   "report.placeholder": "Dodatni komentari",
   "report.submit": "Pošalji",
@@ -179,6 +183,7 @@
   "status.load_more": "Učitaj više",
   "status.media_hidden": "Sakriven media sadržaj",
   "status.mention": "Spomeni @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Utišaj razgovor",
   "status.open": "Proširi ovaj status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 2296ea71e..5892e606e 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -82,7 +82,6 @@
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Mégsem",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
@@ -179,6 +183,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Említés",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index cc48aa996..f73ef0e19 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!",
   "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.",
   "empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "linimasa publik",
   "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.",
   "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Publik",
   "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik",
   "privacy.unlisted.short": "Tak Terdaftar",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Batal",
   "report.placeholder": "Komentar tambahan",
   "report.submit": "Kirim",
@@ -179,6 +183,7 @@
   "status.load_more": "Tampilkan semua",
   "status.media_hidden": "Media disembunyikan",
   "status.mention": "Balasan @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Tampilkan status ini",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index b484bebc7..53371bece 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -82,7 +82,6 @@
   "empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!",
   "empty_column.hashtag": "Esas ankore nulo en ta gretovorto.",
   "empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "la publika tempolineo",
   "empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.",
   "empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Publike",
   "privacy.unlisted.long": "Ne montrar en publika tempolinei",
   "privacy.unlisted.short": "Ne enlistigota",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Nihiligar",
   "report.placeholder": "Plusa komenti",
   "report.submit": "Sendar",
@@ -179,6 +183,7 @@
   "status.load_more": "Kargar pluse",
   "status.media_hidden": "Kontenajo celita",
   "status.mention": "Mencionar @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Detaligar ca mesajo",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 4d73fbea8..3873d797e 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -82,7 +82,6 @@
   "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!",
   "empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.",
   "empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "la timeline pubblica",
   "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
   "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Pubblico",
   "privacy.unlisted.long": "Non mostrare sulla timeline pubblica",
   "privacy.unlisted.short": "Non elencato",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Annulla",
   "report.placeholder": "Commenti aggiuntivi",
   "report.submit": "Invia",
@@ -179,6 +183,7 @@
   "status.load_more": "Mostra di più",
   "status.media_hidden": "Allegato nascosto",
   "status.mention": "Nomina @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Espandi questo post",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index ce797a7c7..fb6d11ebe 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -82,7 +82,6 @@
   "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
   "empty_column.hashtag": "このハッシュタグはまだ使われていません。",
   "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
-  "empty_column.home.inactivity": "あなたのホームフィードにはなにもありません。あなたがしばらくの間アクティブではなかった場合はすぐ元通りになります。",
   "empty_column.home.public_timeline": "連合タイムライン",
   "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
   "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
@@ -184,6 +183,7 @@
   "status.load_more": "もっと見る",
   "status.media_hidden": "非表示のメディア",
   "status.mention": "返信",
+  "status.more": "もっと見る",
   "status.mute_conversation": "会話をミュート",
   "status.open": "詳細を表示",
   "status.pin": "プロフィールに固定表示",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index c1768cf8f..d99dacd59 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -18,7 +18,7 @@
   "account.unblock_domain": "{domain} 숨김 해제",
   "account.unfollow": "팔로우 해제",
   "account.unmute": "뮤트 해제",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "전체 프로필 보기",
   "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
   "bundle_column_error.body": "Something went wrong while loading this component.",
   "bundle_column_error.retry": "Try again",
@@ -33,7 +33,7 @@
   "column.home": "홈",
   "column.mutes": "뮤트 중인 사용자",
   "column.notifications": "알림",
-  "column.pins": "고정된 Toot",
+  "column.pins": "고정된 툿",
   "column.public": "연합 타임라인",
   "column_back_button.label": "돌아가기",
   "column_header.hide_settings": "Hide settings",
@@ -47,7 +47,7 @@
   "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
   "compose_form.lock_disclaimer.lock": "비공개",
   "compose_form.placeholder": "지금 무엇을 하고 있나요?",
-  "compose_form.publish": "Toot",
+  "compose_form.publish": "툿",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
   "compose_form.spoiler": "텍스트 숨기기",
@@ -63,8 +63,8 @@
   "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
+  "embed.preview": "다음과 같이 표시됩니다:",
   "emoji_button.activity": "활동",
   "emoji_button.custom": "Custom",
   "emoji_button.flags": "국기",
@@ -82,7 +82,6 @@
   "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
   "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
   "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
-  "empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.",
   "empty_column.home.public_timeline": "연합 타임라인",
   "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
   "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
@@ -113,7 +112,7 @@
   "navigation_bar.info": "이 인스턴스에 대해서",
   "navigation_bar.logout": "로그아웃",
   "navigation_bar.mutes": "뮤트 중인 사용자",
-  "navigation_bar.pins": "고정된 Toot",
+  "navigation_bar.pins": "고정된 툿",
   "navigation_bar.preferences": "사용자 설정",
   "navigation_bar.public_timeline": "연합 타임라인",
   "notification.favourite": "{name}님이 즐겨찾기 했습니다",
@@ -159,29 +158,35 @@
   "privacy.public.long": "공개 타임라인에 표시",
   "privacy.public.short": "공개",
   "privacy.unlisted.long": "공개 타임라인에 표시하지 않음",
-  "privacy.unlisted.short": "Unlisted",
+  "privacy.unlisted.short": "타임라인에 비표시",
+  "relative_time.days": "{number}일 전",
+  "relative_time.hours": "{number}시간 전",
+  "relative_time.just_now": "방금",
+  "relative_time.minutes": "{number}분 전",
+  "relative_time.seconds": "{number}초 전",
   "reply_indicator.cancel": "취소",
   "report.placeholder": "코멘트",
   "report.submit": "신고하기",
   "report.target": "문제가 된 사용자",
   "search.placeholder": "검색",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.search_format": "고급 검색 방법",
+  "search_popout.tips.hashtag": "해시태그",
+  "search_popout.tips.status": "툿",
+  "search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
+  "search_popout.tips.user": "유저",
   "search_results.total": "{count, number}건의 결과",
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
   "status.delete": "삭제",
-  "status.embed": "Embed",
+  "status.embed": "공유하기",
   "status.favourite": "즐겨찾기",
   "status.load_more": "더 보기",
   "status.media_hidden": "미디어 숨겨짐",
   "status.mention": "답장",
+  "status.more": "More",
   "status.mute_conversation": "이 대화를 뮤트",
   "status.open": "상세 정보 표시",
-  "status.pin": "Pin on profile",
+  "status.pin": "고정",
   "status.reblog": "부스트",
   "status.reblogged_by": "{name}님이 부스트 했습니다",
   "status.reply": "답장",
@@ -193,7 +198,7 @@
   "status.show_less": "숨기기",
   "status.show_more": "더 보기",
   "status.unmute_conversation": "이 대화의 뮤트 해제하기",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "고정 해제",
   "tabs_bar.compose": "포스트",
   "tabs_bar.federated_timeline": "연합",
   "tabs_bar.home": "홈",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 04b88da34..d0727a24d 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -82,7 +82,6 @@
   "empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
   "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
-  "empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
   "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
   "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Openbaar",
   "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen",
   "privacy.unlisted.short": "Minder openbaar",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Annuleren",
   "report.placeholder": "Extra opmerkingen",
   "report.submit": "Verzenden",
@@ -179,6 +183,7 @@
   "status.load_more": "Meer laden",
   "status.media_hidden": "Media verborgen",
   "status.mention": "Vermeld @{name}",
+  "status.more": "Meer",
   "status.mute_conversation": "Negeer conversatie",
   "status.open": "Toot volledig tonen",
   "status.pin": "Aan profielpagina vastmaken",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 26556b290..d74ae0091 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
   "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
   "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "en offentlig tidslinje",
   "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
   "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Offentlig",
   "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
   "privacy.unlisted.short": "Uoppført",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Avbryt",
   "report.placeholder": "Tilleggskommentarer",
   "report.submit": "Send inn",
@@ -179,6 +183,7 @@
   "status.load_more": "Last mer",
   "status.media_hidden": "Media skjult",
   "status.mention": "Nevn @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Demp samtale",
   "status.open": "Utvid denne statusen",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 4715f60ef..1e0849d95 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
   "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
-  "empty_column.home.inactivity": "Vòstra pagina d’acuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
   "empty_column.home.public_timeline": "lo flux public",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
   "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
   "privacy.unlisted.short": "Pas-listat",
+  "relative_time.days": "fa {number}j",
+  "relative_time.hours": "fa {number}h",
+  "relative_time.just_now": "ara",
+  "relative_time.minutes": "fa {number} minutas",
+  "relative_time.seconds": "fa {number} segondas",
   "reply_indicator.cancel": "Anullar",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Mandar",
@@ -179,6 +183,7 @@
   "status.load_more": "Cargar mai",
   "status.media_hidden": "Mèdia rescondut",
   "status.mention": "Mencionar",
+  "status.more": "Mai",
   "status.mute_conversation": "Rescondre la conversacion",
   "status.open": "Desplegar aqueste estatut",
   "status.pin": "Penjar al perfil",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index c8228c0cb..c0776dfc9 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
   "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
   "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
-  "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.",
   "empty_column.home.public_timeline": "publiczna oś czasu",
   "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
   "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Publiczny",
   "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
   "privacy.unlisted.short": "Niewidoczny",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Anuluj",
   "report.placeholder": "Dodatkowe komentarze",
   "report.submit": "Wyślij",
@@ -179,6 +183,7 @@
   "status.load_more": "Załaduj więcej",
   "status.media_hidden": "Zawartość multimedialna ukryta",
   "status.mention": "Wspomnij o @{name}",
+  "status.more": "Więcej",
   "status.mute_conversation": "Wycisz konwersację",
   "status.open": "Rozszerz ten wpis",
   "status.pin": "Przypnij do profilu",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 2c79a7509..ddb8b83f5 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -82,7 +82,6 @@
   "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
   "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag",
   "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
-  "empty_column.home.inactivity": "A sua página inicial está vazia. Se você esteve inativo por um tempo, ela irá se regenerar em alguns intantes.",
   "empty_column.home.public_timeline": "global",
   "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
   "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
@@ -160,16 +159,21 @@
   "privacy.public.short": "Pública",
   "privacy.unlisted.long": "Não publicar em feeds públicos",
   "privacy.unlisted.short": "Não listada",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
-"search_popout.search_format": "Advanced search format",
-"search_popout.tips.hashtag": "hashtag",
-"search_popout.tips.status": "status",
+  "search_popout.search_format": "Formato de busca avançado",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
   "search_popout.tips.text": "Texto simples retorna nomes de exibição, usuários e hashtags correspondentes",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.user": "usuário",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Dê uma espiada...",
   "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
@@ -179,6 +183,7 @@
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Mídia escondida",
   "status.mention": "Mencionar @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Fixar no perfil",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index ecd0689df..9ae140df9 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
   "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
   "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "global",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
   "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Público",
   "privacy.unlisted.long": "Não publicar nos feeds públicos",
   "privacy.unlisted.short": "Não listar",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
@@ -179,6 +183,7 @@
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
   "status.mention": "Mencionar @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expandir",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index bf32c820d..104b063f5 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
   "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.",
   "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.",
-  "empty_column.home.inactivity": "Ваша домашняя лента пуста. Если Вы некоторое время были неактивны, она будет сгенерирована для Вас в ближайшее время.",
   "empty_column.home.public_timeline": "публичные ленты",
   "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.",
   "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Публичный",
   "privacy.unlisted.long": "Не показывать в лентах",
   "privacy.unlisted.short": "Скрытый",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Отмена",
   "report.placeholder": "Комментарий",
   "report.submit": "Отправить",
@@ -179,6 +183,7 @@
   "status.load_more": "Показать еще",
   "status.media_hidden": "Медиаконтент скрыт",
   "status.mention": "Упомянуть @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Заглушить тред",
   "status.open": "Развернуть статус",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
new file mode 100644
index 000000000..70beb70f7
--- /dev/null
+++ b/app/javascript/mastodon/locales/sv.json
@@ -0,0 +1,217 @@
+{
+  "account.block": "Blockera @{name}",
+  "account.block_domain": "Dölj allt från {domain}",
+  "account.disclaimer_full": "Informationen nedan kan spegla användarens profil ofullständigt.",
+  "account.edit_profile": "Redigera profil",
+  "account.follow": "Följ",
+  "account.followers": "Följare",
+  "account.follows": "Följer",
+  "account.follows_you": "Följer dig",
+  "account.media": "Media",
+  "account.mention": "Nämna @{name}",
+  "account.mute": "Tysta @{name}",
+  "account.posts": "Inlägg",
+  "account.report": "Rapportera @{name}",
+  "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
+  "account.share": "Dela @{name}'s profil",
+  "account.unblock": "Avblockera @{name}",
+  "account.unblock_domain": "Ta fram {domain}",
+  "account.unfollow": "Sluta följa",
+  "account.unmute": "Ta bort tystad @{name}",
+  "account.view_full_profile": "Visa hela profilen",
+  "boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
+  "bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
+  "bundle_column_error.retry": "Försök igen",
+  "bundle_column_error.title": "Nätverksfel",
+  "bundle_modal_error.close": "Stäng",
+  "bundle_modal_error.message": "Något gick fel när du laddade denna komponent.",
+  "bundle_modal_error.retry": "Försök igen",
+  "column.blocks": "Blockerade användare",
+  "column.community": "Lokal tidslinje",
+  "column.favourites": "Favoriter",
+  "column.follow_requests": "Följ förfrågningar",
+  "column.home": "Hem",
+  "column.mutes": "Tystade användare",
+  "column.notifications": "Meddelanden",
+  "column.pins": "Nålade toots",
+  "column.public": "Förenad tidslinje",
+  "column_back_button.label": "Tillbaka",
+  "column_header.hide_settings": "Dölj inställningar",
+  "column_header.moveLeft_settings": "Flytta kolumnen till vänster",
+  "column_header.moveRight_settings": "Flytta kolumnen till höger",
+  "column_header.pin": "Fäst",
+  "column_header.show_settings": "Visa inställningar",
+  "column_header.unpin": "Ångra fäst",
+  "column_subheading.navigation": "Navigation",
+  "column_subheading.settings": "Inställningar",
+  "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.",
+  "compose_form.lock_disclaimer.lock": "låst",
+  "compose_form.placeholder": "Vad funderar du på?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive": "Markera media som känslig",
+  "compose_form.spoiler": "Dölj text bakom varning",
+  "compose_form.spoiler_placeholder": "Skriv din varning här",
+  "confirmation_modal.cancel": "Ångra",
+  "confirmations.block.confirm": "Blockera",
+  "confirmations.block.message": "Är du säker att du vill blockera {name}?",
+  "confirmations.delete.confirm": "Ta bort",
+  "confirmations.delete.message": "Är du säker att du vill ta bort denna status?",
+  "confirmations.domain_block.confirm": "Blockera hela domänen",
+  "confirmations.domain_block.message": "Är du verkligen, verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade tillräckligt och föredras.",
+  "confirmations.mute.confirm": "Tysta",
+  "confirmations.mute.message": "Är du säker du vill tysta ner {name}?",
+  "confirmations.unfollow.confirm": "Sluta följa",
+  "confirmations.unfollow.message": "Är du säker på att du vill sluta följa {name}?",
+  "embed.instructions": "Bädda in den här statusen på din webbplats genom att kopiera koden nedan.",
+  "embed.preview": "Här ser du hur det kommer att se ut:",
+  "emoji_button.activity": "Aktivitet",
+  "emoji_button.custom": "Specialgjord",
+  "emoji_button.flags": "Flaggor",
+  "emoji_button.food": "Mat & Dryck",
+  "emoji_button.label": "Lägg till emoji",
+  "emoji_button.nature": "Natur",
+  "emoji_button.not_found": "Inga emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objekt",
+  "emoji_button.people": "Människor",
+  "emoji_button.recent": "Ofta använda",
+  "emoji_button.search": "Sök...",
+  "emoji_button.search_results": "Sökresultat",
+  "emoji_button.symbols": "Symboler",
+  "emoji_button.travel": "Resor & Platser",
+  "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!",
+  "empty_column.hashtag": "Det finns inget i denna hashtag ännu.",
+  "empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.",
+  "empty_column.home.inactivity": "Ditt hemmafeed är tomt. Om du har varit inaktiv ett tag kommer det att regenereras för dig snart.",
+  "empty_column.home.public_timeline": "den publika tidslinjen",
+  "empty_column.notifications": "Du har inga meddelanden än. Interagera med andra för att starta konversationen.",
+  "empty_column.public": "Det finns inget här! Skriv något offentligt, eller följ manuellt användarna från andra instanser för att fylla på det",
+  "follow_request.authorize": "Godkänn",
+  "follow_request.reject": "Avvisa",
+  "getting_started.appsshort": "Appar",
+  "getting_started.faq": "FAQ",
+  "getting_started.heading": "Kom igång",
+  "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem på GitHub på {github}.",
+  "getting_started.userguide": "Användarguide",
+  "home.column_settings.advanced": "Avancerad",
+  "home.column_settings.basic": "Grundläggande",
+  "home.column_settings.filter_regex": "Filtrera ut med regelbundna uttryck",
+  "home.column_settings.show_reblogs": "Visa knuffar",
+  "home.column_settings.show_replies": "Visa svar",
+  "home.settings": "Kolumninställningar",
+  "lightbox.close": "Stäng",
+  "lightbox.next": "Nästa",
+  "lightbox.previous": "Tidigare",
+  "loading_indicator.label": "Laddar...",
+  "media_gallery.toggle_visible": "Växla synlighet",
+  "missing_indicator.label": "Hittades inte",
+  "navigation_bar.blocks": "Blockerade användare",
+  "navigation_bar.community_timeline": "Lokal tidslinje",
+  "navigation_bar.edit_profile": "Redigera profil",
+  "navigation_bar.favourites": "Favoriter",
+  "navigation_bar.follow_requests": "Följförfrågningar",
+  "navigation_bar.info": "Om denna instans",
+  "navigation_bar.logout": "Logga ut",
+  "navigation_bar.mutes": "Tystade användare",
+  "navigation_bar.pins": "Nålade inlägg (toots)",
+
+  "navigation_bar.preferences": "Inställningar",
+  "navigation_bar.public_timeline": "Förenad tidslinje",
+  "notification.favourite": "{name} favoriserade din status",
+  "notification.follow": "{name} följer dig",
+  "notification.mention": "{name} nämnde dig",
+  "notification.reblog": "{name} knuffade din status",
+  "notifications.clear": "Rensa meddelanden",
+  "notifications.clear_confirmation": "Är du säker på att du vill radera alla dina meddelanden permanent?",
+  "notifications.column_settings.alert": "Skrivbordsmeddelanden",
+  "notifications.column_settings.favourite": "Favoriter:",
+  "notifications.column_settings.follow": "Nya följare:",
+  "notifications.column_settings.mention": "Omnämningar:",
+  "notifications.column_settings.push": "Push meddelanden",
+  "notifications.column_settings.push_meta": "Denna anordning",
+  "notifications.column_settings.reblog": "Knuffar:",
+  "notifications.column_settings.show": "Visa i kolumnen",
+  "notifications.column_settings.sound": "Spela upp ljud",
+  "onboarding.done": "Klart",
+  "onboarding.next": "Nästa",
+  "onboarding.page_five.public_timelines": "Den lokala tidslinjen visar offentliga inlägg från alla på {domain}. Den förenade tidslinjen visar offentliga inlägg från alla personer på {domain} som följer. Dom här offentliga tidslinjerna är ett bra sätt att upptäcka nya människor.",
+  "onboarding.page_four.home": "Hemmatidslinjen visar inlägg från personer du följer.",
+  "onboarding.page_four.notifications": "Meddelandekolumnen visar när någon interagerar med dig.",
+  "onboarding.page_one.federation": "Mastodon är ett nätverk av oberoende servrar som ansluter för att skapa ett större socialt nätverk. Vi kallar dessa servrar instanser.",
+  "onboarding.page_one.handle": "Du är på {domain}, så din fulla hantering är {handle}",
+  "onboarding.page_one.welcome": "Välkommen till Mastodon!",
+  "onboarding.page_six.admin": "Din instansadmin är {admin}.",
+  "onboarding.page_six.almost_done": "Snart klart...",
+  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.apps_available": "Det finns {apps} tillgängligt för iOS, Android och andra plattformar.",
+  "onboarding.page_six.github": "Mastodon är fri programvara med öppen källkod. Du kan rapportera fel, efterfråga funktioner eller bidra till koden på {github}.",
+  "onboarding.page_six.guidelines": "gemenskapsriktlinjer",
+  "onboarding.page_six.read_guidelines": "Vänligen läs {domain}'s {guidelines}!",
+  "onboarding.page_six.various_app": "mobilappar",
+  "onboarding.page_three.profile": "Redigera din profil för att ändra ditt avatar, bio och visningsnamn. Där hittar du även andra inställningar.",
+  "onboarding.page_three.search": "Använd sökfältet för att hitta personer och titta på hashtags, till exempel {illustration} och {introductions}. För att leta efter en person som inte befinner sig i detta fall använd deras fulla handhavande.",
+  "onboarding.page_two.compose": "Skriv inlägg från skrivkolumnen. Du kan ladda upp bilder, ändra integritetsinställningar och lägga till varningar med ikonerna nedan.",
+  "onboarding.skip": "Hoppa över",
+  "privacy.change": "Justera status sekretess",
+  "privacy.direct.long": "Skicka endast till nämnda användare",
+  "privacy.direct.short": "Direkt",
+  "privacy.private.long": "Skicka endast till följare",
+  "privacy.private.short": "Endast följare",
+  "privacy.public.long": "Skicka till publik tidslinje",
+  "privacy.public.short": "Publik",
+  "privacy.unlisted.long": "Skicka inte till publik tidslinje",
+  "privacy.unlisted.short": "Olistad",
+  "reply_indicator.cancel": "Ångra",
+  "report.placeholder": "Ytterligare kommentarer",
+  "report.submit": "Skicka",
+  "report.target": "Rapporterar {target}",
+  "search.placeholder": "Sök",
+  "search_popout.search_format": "Avancerat sökformat",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Enkel text returnerar matchande visningsnamn, användarnamn och hashtags",
+  "search_popout.tips.user": "användare",
+  "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}",
+  "standalone.public_title": "En titt inuti...",
+  "status.cannot_reblog": "Detta inlägg kan inte knuffas",
+  "status.delete": "Ta bort",
+  "status.embed": "Bädda in",
+  "status.favourite": "Favorit",
+  "status.load_more": "Ladda fler",
+  "status.media_hidden": "Media dold",
+  "status.mention": "Omnämn @{name}",
+  "status.mute_conversation": "Tysta konversation",
+  "status.open": "Utvidga denna status",
+  "status.pin": "Fäst i profil",
+  "status.reblog": "Knuff",
+  "status.reblogged_by": "{name} knuffade",
+  "status.reply": "Svara",
+  "status.replyAll": "Svara på tråden",
+  "status.report": "Rapportera @{name}",
+  "status.sensitive_toggle": "Klicka för att se",
+  "status.sensitive_warning": "Känsligt innehåll",
+  "status.share": "Dela",
+  "status.show_less": "Visa mindre",
+  "status.show_more": "Visa mer",
+  "status.unmute_conversation": "Öppna konversation",
+  "status.unpin": "Ångra fäst i profil",
+  "tabs_bar.compose": "Skriv",
+  "tabs_bar.federated_timeline": "Förenad",
+  "tabs_bar.home": "Hem",
+  "tabs_bar.local_timeline": "Lokal",
+  "tabs_bar.notifications": "Meddelanden",
+  "upload_area.title": "Dra & släpp för att ladda upp",
+  "upload_button.label": "Lägg till media",
+  "upload_form.description": "Beskriv för synskadade",
+  "upload_form.undo": "Ångra",
+  "upload_progress.label": "Laddar upp...",
+  "video.close": "Stäng video",
+  "video.exit_fullscreen": "Stäng helskärm",
+  "video.expand": "Expandera video",
+  "video.fullscreen": "Helskärm",
+  "video.hide": "Dölj video",
+  "video.mute": "Tysta ljud",
+  "video.pause": "Pause",
+  "video.play": "Spela upp",
+  "video.unmute": "Spela upp ljud"
+}
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 4339d1497..db3f3dd0d 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -82,7 +82,6 @@
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not post to public timelines",
   "privacy.unlisted.short": "Unlisted",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancel",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
@@ -179,6 +183,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mention @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index afc6383b4..cdd3581da 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Yerel zaman tüneliniz boş. Daha fazla eğlence için herkese açık bir gönderi paylaşın.",
   "empty_column.hashtag": "Henüz bu hashtag’e sahip hiçbir gönderi yok.",
   "empty_column.home": "Henüz kimseyi takip etmiyorsunuz. {public} ziyaret edebilir veya arama kısmını kullanarak diğer kullanıcılarla iletişime geçebilirsiniz.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "herkese açık zaman tüneli",
   "empty_column.notifications": "Henüz hiçbir bildiriminiz yok. Diğer insanlarla sobhet edebilmek için etkileşime geçebilirsiniz.",
   "empty_column.public": "Burada hiçbir gönderi yok! Herkese açık bir şeyler yazın, veya diğer sunucudaki insanları takip ederek bu alanın dolmasını sağlayın",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Herkese açık",
   "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme",
   "privacy.unlisted.short": "Listelenmemiş",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "İptal",
   "report.placeholder": "Ek yorumlar",
   "report.submit": "Gönder",
@@ -179,6 +183,7 @@
   "status.load_more": "Daha fazla",
   "status.media_hidden": "Gizli görsel",
   "status.mention": "Bahset @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Bu gönderiyi genişlet",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index d0aae032b..930529f15 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -82,7 +82,6 @@
   "empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!",
   "empty_column.hashtag": "Дописів з цим хештегом поки не існує.",
   "empty_column.home": "Ви поки ні на кого не підписані. Погортайте {public}, або скористуйтесь пошуком, щоб освоїтися та познайомитися з іншими користувачами.",
-  "empty_column.home.inactivity": "Ваша домашня стрічка пуста. Якщо ви були неактивні протягом деякого часу, вона скоро буде згенерована для Вас.",
   "empty_column.home.public_timeline": "публічні стрічки",
   "empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.",
   "empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку.",
@@ -160,6 +159,11 @@
   "privacy.public.short": "Публічний",
   "privacy.unlisted.long": "Не показувати у публічних стрічках",
   "privacy.unlisted.short": "Прихований",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Відмінити",
   "report.placeholder": "Додаткові коментарі",
   "report.submit": "Відправити",
@@ -179,6 +183,7 @@
   "status.load_more": "Завантажити більше",
   "status.media_hidden": "Медіаконтент приховано",
   "status.mention": "Згадати",
+  "status.more": "More",
   "status.mute_conversation": "Заглушити діалог",
   "status.open": "Розгорнути допис",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index e0ffc16df..827c815cf 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -82,7 +82,6 @@
   "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
   "empty_column.hashtag": "这个标签暂时未有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
-  "empty_column.home.inactivity": "你的主页暂时没有内容。也许你太久没有来了?如果是这样,文章会慢慢出来,请稍后再看。",
   "empty_column.home.public_timeline": "公共时间轴",
   "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
   "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
@@ -160,6 +159,11 @@
   "privacy.public.short": "公共",
   "privacy.unlisted.long": "公开,但不在公共时间轴显示",
   "privacy.unlisted.short": "公开",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "取消",
   "report.placeholder": "额外消息",
   "report.submit": "提交",
@@ -179,6 +183,7 @@
   "status.load_more": "加载更多",
   "status.media_hidden": "隐藏媒体内容",
   "status.mention": "提及 @{name}",
+  "status.more": "More",
   "status.mute_conversation": "静音对话",
   "status.open": "展开嘟文",
   "status.pin": "置顶到资料",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 053e971aa..f6f56fee1 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -82,7 +82,6 @@
   "empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!",
   "empty_column.hashtag": "這個標籤暫時未有內容。",
   "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
-  "empty_column.home.inactivity": "你的主頁暫時沒有內容。也許你太久沒有來?如果是這樣,文章會慢慢出來,請稍後再看。",
   "empty_column.home.public_timeline": "公共時間軸",
   "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
   "empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
@@ -160,6 +159,11 @@
   "privacy.public.short": "公共",
   "privacy.unlisted.long": "公開,但不在公共時間軸顯示",
   "privacy.unlisted.short": "公開",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "取消",
   "report.placeholder": "額外訊息",
   "report.submit": "提交",
@@ -179,6 +183,7 @@
   "status.load_more": "載入更多",
   "status.media_hidden": "隱藏媒體內容",
   "status.mention": "提及 @{name}",
+  "status.more": "More",
   "status.mute_conversation": "靜音對話",
   "status.open": "展開文章",
   "status.pin": "置頂到資料頁",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index a22d66fa1..1f43c6a20 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -82,7 +82,6 @@
   "empty_column.community": "本地時間軸是空的。公開寫點什麼吧!",
   "empty_column.hashtag": "這個主題標籤下什麼都沒有。",
   "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。",
-  "empty_column.home.inactivity": "你家的訊息摘要是空的。如果你很久沒活動了,很快它就會重新產生。",
   "empty_column.home.public_timeline": "公開時間軸",
   "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
   "empty_column.public": "這裡什麼都沒有!公開寫些什麼,或是關注其他副本的使用者。",
@@ -160,6 +159,11 @@
   "privacy.public.short": "公開貼",
   "privacy.unlisted.long": "不要貼到公開時間軸",
   "privacy.unlisted.short": "不列出來",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "取消",
   "report.placeholder": "更多訊息",
   "report.submit": "送出",
@@ -179,6 +183,7 @@
   "status.load_more": "載入更多",
   "status.media_hidden": "媒體已隱藏",
   "status.mention": "提到 @{name}",
+  "status.more": "More",
   "status.mute_conversation": "消音對話",
   "status.open": "展開這個狀態",
   "status.pin": "置頂到個人資訊頁",
diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index c85cd5800..93d2eaf10 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -1,6 +1,5 @@
-import * as OfflinePluginRuntime from 'offline-plugin/runtime';
 import * as WebPushSubscription from './web_push_subscription';
-import Mastodon from 'mastodon/containers/mastodon';
+import Mastodon from './containers/mastodon';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ready from './ready';
@@ -25,7 +24,7 @@ function main() {
     ReactDOM.render(<Mastodon {...props} />, mountNode);
     if (process.env.NODE_ENV === 'production') {
       // avoid offline in dev mode because it's harder to debug
-      OfflinePluginRuntime.install();
+      require('offline-plugin/runtime').install();
       WebPushSubscription.register();
     }
     perf.stop('main()');
diff --git a/app/javascript/mastodon/middleware/sounds.js b/app/javascript/mastodon/middleware/sounds.js
index 372e7c835..3d1e3eaba 100644
--- a/app/javascript/mastodon/middleware/sounds.js
+++ b/app/javascript/mastodon/middleware/sounds.js
@@ -12,7 +12,11 @@ const createAudio = sources => {
 const play = audio => {
   if (!audio.paused) {
     audio.pause();
-    audio.fastSeek(0);
+    if (typeof audio.fastSeek === 'function') {
+      audio.fastSeek(0);
+    } else {
+      audio.seek(0);
+    }
   }
 
   audio.play();
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index b17d74ef3..bee4c4ef9 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -31,10 +31,10 @@ const initialTimeline = ImmutableMap({
 });
 
 const normalizeTimeline = (state, timeline, statuses, next) => {
-  const ids       = ImmutableList(statuses.map(status => status.get('id')));
+  const oldIds    = state.getIn([timeline, 'items'], ImmutableList());
+  const ids       = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId));
   const wasLoaded = state.getIn([timeline, 'loaded']);
   const hadNext   = state.getIn([timeline, 'next']);
-  const oldIds    = state.getIn([timeline, 'items'], ImmutableList());
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('loaded', true);
@@ -45,8 +45,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => {
 };
 
 const appendNormalizedTimeline = (state, timeline, statuses, next) => {
-  const ids    = ImmutableList(statuses.map(status => status.get('id')));
   const oldIds = state.getIn([timeline, 'items'], ImmutableList());
+  const ids    = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId));
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('isLoading', false);
@@ -75,15 +75,9 @@ const updateTimeline = (state, timeline, status, references) => {
   }));
 };
 
-const deleteStatus = (state, id, accountId, references, reblogOf) => {
+const deleteStatus = (state, id, accountId, references) => {
   state.keySeq().forEach(timeline => {
-    state = state.updateIn([timeline, 'items'], list => {
-      if (reblogOf && !list.includes(reblogOf)) {
-        return list.map(item => item === id ? reblogOf : item);
-      } else {
-        return list.filterNot(item => item === id);
-      }
-    });
+    state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
   });
 
   // Remove reblogs of deleted status
diff --git a/app/javascript/mastodon/test_setup.js b/app/javascript/mastodon/test_setup.js
new file mode 100644
index 000000000..80148379b
--- /dev/null
+++ b/app/javascript/mastodon/test_setup.js
@@ -0,0 +1,5 @@
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+const adapter = new Adapter();
+configure({ adapter });
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index aa94006c6..d275c3bb0 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -2,7 +2,8 @@ import loadPolyfills from '../mastodon/load_polyfills';
 
 // import default stylesheet with variables
 require('font-awesome/css/font-awesome.css');
-import 'styles/application';
+
+import '../styles/application.scss';
 
 require.context('../images/', true);
 
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
index de0c68fa5..5ac6504d4 100644
--- a/app/javascript/packs/common.js
+++ b/app/javascript/packs/common.js
@@ -1,6 +1,9 @@
 import { start } from 'rails-ujs';
+import 'font-awesome/css/font-awesome.css';
 
 // import common styling
 require('../styles/common.scss');
 
+require.context('../images/', true);
+
 start();
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index e35937be1..efd34393f 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -1,20 +1,23 @@
-@import 'mixins';
-@import 'variables';
+@import 'mastodon/mixins';
+@import 'mastodon/variables';
 @import 'variables-glitch';
+@import 'fonts/roboto';
+@import 'fonts/roboto-mono';
+@import 'fonts/montserrat';
 
-@import 'reset';
-@import 'basics';
-@import 'containers';
-@import 'lists';
-@import 'footer';
-@import 'compact_header';
-@import 'landing_strip';
-@import 'forms';
-@import 'accounts';
-@import 'stream_entries';
-@import 'components';
-@import 'emoji_picker';
-@import 'about';
-@import 'tables';
-@import 'admin';
-@import 'rtl';
+@import 'mastodon/reset';
+@import 'mastodon/basics';
+@import 'mastodon/containers';
+@import 'mastodon/lists';
+@import 'mastodon/footer';
+@import 'mastodon/compact_header';
+@import 'mastodon/landing_strip';
+@import 'mastodon/forms';
+@import 'mastodon/accounts';
+@import 'mastodon/stream_entries';
+@import 'mastodon/components';
+@import 'mastodon/emoji_picker';
+@import 'mastodon/about';
+@import 'mastodon/tables';
+@import 'mastodon/admin';
+@import 'mastodon/rtl';
diff --git a/app/javascript/styles/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss
index 7412991b8..7412991b8 100644
--- a/app/javascript/styles/_mixins.scss
+++ b/app/javascript/styles/mastodon/_mixins.scss
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/mastodon/about.scss
index 4ec689427..4ec689427 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index b00dd8c1e..b00dd8c1e 100644
--- a/app/javascript/styles/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
diff --git a/app/javascript/styles/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 87bc710af..87bc710af 100644
--- a/app/javascript/styles/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 43c32c8bc..b5d77ff63 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -30,7 +30,7 @@ body {
   }
 
   &.app-body {
-    position: fixed;
+    position: absolute;
     width: 100%;
     height: 100%;
     padding: 0;
diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/mastodon/boost.scss
index b07b72f8e..b07b72f8e 100644
--- a/app/javascript/styles/boost.scss
+++ b/app/javascript/styles/mastodon/boost.scss
diff --git a/app/javascript/styles/compact_header.scss b/app/javascript/styles/mastodon/compact_header.scss
index 90d98cc8c..90d98cc8c 100644
--- a/app/javascript/styles/compact_header.scss
+++ b/app/javascript/styles/mastodon/compact_header.scss
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/mastodon/components.scss
index 7ef3dcc43..6fe179581 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -302,7 +302,6 @@
   font-family: inherit;
   font-size: 14px;
   background: $simple-background-color;
-  border-radius: 0 0 4px;
 }
 
 .compose-form__buttons-wrapper {
@@ -452,6 +451,7 @@
 
 .compose-form__publish {
   display: flex;
+  justify-content: flex-end;
   min-width: 0;
 }
 
@@ -468,7 +468,7 @@
 
 .compose-form__publish__side-arm {
   padding: 0 !important;
-  width: 4em;
+  width: 36px;
   text-align: center;
   margin-right: 2px;
 }
@@ -551,6 +551,14 @@
   overflow: visible;
   white-space: pre-wrap;
 
+  &.status__content--with-spoiler {
+    white-space: normal;
+
+    .status__content__text {
+      white-space: pre-wrap;
+    }
+  }
+
   .emojione {
     width: 18px;
     height: 18px;
diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/mastodon/containers.scss
index af2589e23..af2589e23 100644
--- a/app/javascript/styles/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
diff --git a/app/javascript/styles/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index 2b46d30fc..2b46d30fc 100644
--- a/app/javascript/styles/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
diff --git a/app/javascript/styles/footer.scss b/app/javascript/styles/mastodon/footer.scss
index 2d953b34e..2d953b34e 100644
--- a/app/javascript/styles/footer.scss
+++ b/app/javascript/styles/mastodon/footer.scss
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 61fcf286f..61fcf286f 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
diff --git a/app/javascript/styles/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 15ff84912..15ff84912 100644
--- a/app/javascript/styles/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
diff --git a/app/javascript/styles/lists.scss b/app/javascript/styles/mastodon/lists.scss
index 6019cd800..6019cd800 100644
--- a/app/javascript/styles/lists.scss
+++ b/app/javascript/styles/mastodon/lists.scss
diff --git a/app/javascript/styles/reset.scss b/app/javascript/styles/mastodon/reset.scss
index cc5ba9d7c..cc5ba9d7c 100644
--- a/app/javascript/styles/reset.scss
+++ b/app/javascript/styles/mastodon/reset.scss
diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 67bfa8a38..67bfa8a38 100644
--- a/app/javascript/styles/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index 453070b7c..453070b7c 100644
--- a/app/javascript/styles/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
diff --git a/app/javascript/styles/tables.scss b/app/javascript/styles/mastodon/tables.scss
index ad46f5f9f..ad46f5f9f 100644
--- a/app/javascript/styles/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
diff --git a/app/javascript/styles/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 090706ff5..090706ff5 100644
--- a/app/javascript/styles/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
diff --git a/app/javascript/themes/spin/pack.js b/app/javascript/themes/spin/pack.js
index dab0e93a4..b11ac4802 100644
--- a/app/javascript/themes/spin/pack.js
+++ b/app/javascript/themes/spin/pack.js
@@ -1,2 +1,2 @@
-import 'packs/application';
-import 'themes/spin/style';
+import '../../packs/application';
+import './style.scss';