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.js4
-rw-r--r--app/javascript/glitch/components/status/action_bar.js5
-rw-r--r--app/javascript/glitch/components/status/container.js2
-rw-r--r--app/javascript/glitch/components/status/index.js12
-rw-r--r--app/javascript/mastodon/actions/pin_statuses.js5
-rw-r--r--app/javascript/mastodon/actions/streaming.js57
-rw-r--r--app/javascript/mastodon/components/account.js6
-rw-r--r--app/javascript/mastodon/components/icon_button.js24
-rw-r--r--app/javascript/mastodon/components/media_gallery.js13
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js2
-rw-r--r--app/javascript/mastodon/components/status.js8
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js5
-rw-r--r--app/javascript/mastodon/containers/account_container.js5
-rw-r--r--app/javascript/mastodon/containers/compose_container.js5
-rw-r--r--app/javascript/mastodon/containers/mastodon.js9
-rw-r--r--app/javascript/mastodon/containers/status_container.js22
-rw-r--r--app/javascript/mastodon/containers/timeline_container.js5
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js4
-rw-r--r--app/javascript/mastodon/features/account/components/header.js20
-rw-r--r--app/javascript/mastodon/features/account_gallery/index.js7
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/header.js5
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js5
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js5
-rw-r--r--app/javascript/mastodon/features/blocks/index.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js9
-rw-r--r--app/javascript/mastodon/features/compose/containers/compose_form_container.js1
-rw-r--r--app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js1
-rw-r--r--app/javascript/mastodon/features/compose/containers/navigation_container.js3
-rw-r--r--app/javascript/mastodon/features/compose/containers/warning_container.js3
-rw-r--r--app/javascript/mastodon/features/compose/util/counter.js2
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji-test.js16
-rw-r--r--app/javascript/mastodon/features/emoji/emoji.js38
-rw-r--r--app/javascript/mastodon/features/emoji/emoji_compressed.js4
-rw-r--r--app/javascript/mastodon/features/favourites/index.js2
-rw-r--r--app/javascript/mastodon/features/follow_requests/index.js2
-rw-r--r--app/javascript/mastodon/features/followers/index.js2
-rw-r--r--app/javascript/mastodon/features/following/index.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js9
-rw-r--r--app/javascript/mastodon/features/mutes/index.js2
-rw-r--r--app/javascript/mastodon/features/reblogs/index.js2
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js4
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js2
-rw-r--r--app/javascript/mastodon/features/status/index.js41
-rw-r--r--app/javascript/mastodon/features/ui/components/mute_modal.js8
-rw-r--r--app/javascript/mastodon/features/ui/components/onboarding_modal.js25
-rw-r--r--app/javascript/mastodon/features/ui/containers/status_list_container.js4
-rw-r--r--app/javascript/mastodon/features/ui/index.js30
-rw-r--r--app/javascript/mastodon/features/ui/util/optional_motion.js57
-rw-r--r--app/javascript/mastodon/features/ui/util/react_router_helpers.js18
-rw-r--r--app/javascript/mastodon/features/ui/util/reduced_motion.js44
-rw-r--r--app/javascript/mastodon/initial_state.js21
-rw-r--r--app/javascript/mastodon/locales/ko.json2
-rw-r--r--app/javascript/mastodon/locales/oc.json22
-rw-r--r--app/javascript/mastodon/locales/pl.json4
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json224
-rw-r--r--app/javascript/mastodon/reducers/compose.js3
-rw-r--r--app/javascript/mastodon/reducers/custom_emojis.js2
-rw-r--r--app/javascript/mastodon/reducers/meta.js1
-rw-r--r--app/javascript/mastodon/reducers/mutes.js2
-rw-r--r--app/javascript/mastodon/stream.js61
-rw-r--r--app/javascript/packs/custom.js1
-rw-r--r--app/javascript/styles/mastodon/accounts.scss18
-rw-r--r--app/javascript/styles/mastodon/components.scss40
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss7
-rw-r--r--app/javascript/themes/default/theme.yml17
-rw-r--r--app/javascript/themes/spin/pack.js2
-rw-r--r--app/javascript/themes/spin/style.scss14
-rw-r--r--app/javascript/themes/spin/theme.yml2
70 files changed, 560 insertions, 461 deletions
diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js
index c94fb0851..7bc1a2189 100644
--- a/app/javascript/glitch/components/account/header.js
+++ b/app/javascript/glitch/components/account/header.js
@@ -51,6 +51,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import emojify from '../../../mastodon/features/emoji/emoji';
 import IconButton from '../../../mastodon/components/icon_button';
 import Avatar from '../../../mastodon/components/avatar';
+import { me } from '../../../mastodon/initial_state';
 
 //  Our imports  //
 import { processBio } from '../../util/bio_metadata';
@@ -88,7 +89,6 @@ export default class AccountHeader extends ImmutablePureComponent {
 
   static propTypes = {
     account  : ImmutablePropTypes.map,
-    me       : PropTypes.string.isRequired,
     onFollow : PropTypes.func.isRequired,
     intl     : PropTypes.object.isRequired,
   };
@@ -102,7 +102,7 @@ The `render()` function is used to render our component.
 */
 
   render () {
-    const { account, me, intl } = this.props;
+    const { account, intl } = this.props;
 
 /*
 
diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js
index f4450d31b..34588b008 100644
--- a/app/javascript/glitch/components/status/action_bar.js
+++ b/app/javascript/glitch/components/status/action_bar.js
@@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import RelativeTimestamp from '../../../mastodon/components/relative_timestamp';
 import IconButton from '../../../mastodon/components/icon_button';
 import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container';
+import { me } from '../../../mastodon/initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -50,7 +51,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onEmbed: PropTypes.func,
     onMuteConversation: PropTypes.func,
     onPin: PropTypes.func,
-    me: PropTypes.string,
     withDismiss: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -59,7 +59,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
   // evaluate to false. See react-immutable-pure-component for usage.
   updateOnProps = [
     'status',
-    'me',
     'withDismiss',
   ]
 
@@ -119,7 +118,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   render () {
-    const { status, me, intl, withDismiss } = this.props;
+    const { status, intl, withDismiss } = this.props;
 
     const mutingConversation = status.get('muted');
     const anonymousAccess = !me;
diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js
index 24261e763..0054abd14 100644
--- a/app/javascript/glitch/components/status/container.js
+++ b/app/javascript/glitch/components/status/container.js
@@ -140,12 +140,10 @@ Here are the props we pass to `<Status>`.
     return {
       status      : status,
       account     : account || ownProps.account,
-      me          : state.getIn(['meta', 'me']),
       settings    : state.get('local_settings'),
       prepend     : prepend || ownProps.prepend,
       reblogModal : state.getIn(['meta', 'boost_modal']),
       deleteModal : state.getIn(['meta', 'delete_modal']),
-      autoPlayGif : state.getIn(['meta', 'auto_play_gif']),
     };
   };
 
diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js
index 6bd95b051..33a9730e5 100644
--- a/app/javascript/glitch/components/status/index.js
+++ b/app/javascript/glitch/components/status/index.js
@@ -39,6 +39,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 
 //  Mastodon imports  //
 import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task';
+import { autoPlayGif } from '../../../mastodon/initial_state';
 
 //  Our imports  //
 import StatusPrepend from './prepend';
@@ -89,9 +90,6 @@ few parts:
     These are our local settings, fetched from our store. We need this
     to determine how best to collapse our statuses, among other things.
 
- -  __`me` (`PropTypes.number`) :__
-    This is the id of the currently-signed-in user.
-
  -  __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`,
     `onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`,
     `onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__
@@ -103,9 +101,6 @@ few parts:
     reblogging and deleting statuses. They are used by the `onReblog`
     and `onDelete` functions, but we don't deal with them here.
 
- -  __`autoPlayGif` (`PropTypes.bool`) :__
-    This tells the frontend whether or not to autoplay gifs!
-
  -  __`muted` (`PropTypes.bool`) :__
     This has nothing to do with a user or conversation mute! "Muted" is
     what Mastodon internally calls the subdued look of statuses in the
@@ -160,7 +155,6 @@ export default class Status extends ImmutablePureComponent {
     account                     : ImmutablePropTypes.map,
     settings                    : ImmutablePropTypes.map,
     notification                : ImmutablePropTypes.map,
-    me                          : PropTypes.string,
     onFavourite                 : PropTypes.func,
     onReblog                    : PropTypes.func,
     onModalReblog               : PropTypes.func,
@@ -177,7 +171,6 @@ export default class Status extends ImmutablePureComponent {
     onOpenVideo                 : PropTypes.func,
     reblogModal                 : PropTypes.bool,
     deleteModal                 : PropTypes.bool,
-    autoPlayGif                 : PropTypes.bool,
     muted                       : PropTypes.bool,
     collapse                    : PropTypes.bool,
     prepend                     : PropTypes.string,
@@ -211,9 +204,7 @@ to remember to specify it here.
     'account',
     'settings',
     'prepend',
-    'me',
     'boostModal',
-    'autoPlayGif',
     'muted',
     'collapse',
     'notification',
@@ -560,7 +551,6 @@ this operation are further explained in the code below.
       intersectionObserverWrapper,
       onOpenVideo,
       onOpenMedia,
-      autoPlayGif,
       notification,
       ...other
     } = this.props;
diff --git a/app/javascript/mastodon/actions/pin_statuses.js b/app/javascript/mastodon/actions/pin_statuses.js
index 01bf8930b..3f40f6c2d 100644
--- a/app/javascript/mastodon/actions/pin_statuses.js
+++ b/app/javascript/mastodon/actions/pin_statuses.js
@@ -4,12 +4,13 @@ export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
 export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
 export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
 
+import { me } from '../initial_state';
+
 export function fetchPinnedStatuses() {
   return (dispatch, getState) => {
     dispatch(fetchPinnedStatusesRequest());
 
-    const accountId = getState().getIn(['meta', 'me']);
-    api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => {
+    api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
       dispatch(fetchPinnedStatusesSuccess(response.data, null));
     }).catch(error => {
       dispatch(fetchPinnedStatusesFail(error));
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index a2e25c930..e60ddacd9 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -1,4 +1,4 @@
-import createStream from '../stream';
+import { connectStream } from '../stream';
 import {
   updateTimeline,
   deleteFromTimelines,
@@ -12,42 +12,19 @@ import { getLocale } from '../locales';
 const { messages } = getLocale();
 
 export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
-  return (dispatch, getState) => {
-    const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
-    const accessToken = getState().getIn(['meta', 'access_token']);
-    const locale = getState().getIn(['meta', 'locale']);
-    let polling = null;
-
-    const setupPolling = () => {
-      polling = setInterval(() => {
-        pollingRefresh(dispatch);
-      }, 20000);
-    };
-
-    const clearPolling = () => {
-      if (polling) {
-        clearInterval(polling);
-        polling = null;
-      }
-    };
-
-    const subscription = createStream(streamingAPIBaseURL, accessToken, path, {
 
-      connected () {
-        if (pollingRefresh) {
-          clearPolling();
-        }
+  return connectStream (path, pollingRefresh, (dispatch, getState) => {
+    const locale = getState().getIn(['meta', 'locale']);
+    return {
+      onConnect() {
         dispatch(connectTimeline(timelineId));
       },
 
-      disconnected () {
-        if (pollingRefresh) {
-          setupPolling();
-        }
+      onDisconnect() {
         dispatch(disconnectTimeline(timelineId));
       },
 
-      received (data) {
+      onReceive (data) {
         switch(data.event) {
         case 'update':
           dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
@@ -60,26 +37,8 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
           break;
         }
       },
-
-      reconnected () {
-        if (pollingRefresh) {
-          clearPolling();
-          pollingRefresh(dispatch);
-        }
-        dispatch(connectTimeline(timelineId));
-      },
-
-    });
-
-    const disconnect = () => {
-      if (subscription) {
-        subscription.close();
-      }
-      clearPolling();
     };
-
-    return disconnect;
-  };
+  });
 }
 
 function refreshHomeTimelineAndNotification (dispatch) {
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 376e544fb..2c3a00064 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -7,6 +7,7 @@ import Permalink from './permalink';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { me } from '../initial_state';
 
 const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -23,7 +24,6 @@ export default class Account extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMute: PropTypes.func.isRequired,
@@ -52,7 +52,7 @@ export default class Account extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, me, intl, hidden } = this.props;
+    const { account, intl, hidden } = this.props;
 
     if (!account) {
       return <div />;
@@ -82,7 +82,7 @@ export default class Account extends ImmutablePureComponent {
       } else if (muting) {
         let hidingNotificationsButton;
         if (muting.get('notifications')) {
-          hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username')  })} onClick={this.handleUnmuteNotifications} />;
+          hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
         } else {
           hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
         }
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index 76b0da12f..d0c1b049f 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -65,6 +65,7 @@ export default class IconButton extends React.PureComponent {
       expanded,
       icon,
       inverted,
+      flip,
       overlay,
       pressed,
       tabIndex,
@@ -78,8 +79,8 @@ export default class IconButton extends React.PureComponent {
       overlayed: overlay,
     });
 
-    const flipDeg = this.props.flip ? -180 : -360;
-    const rotateDeg = this.props.active ? flipDeg : 0;
+    const flipDeg = flip ? -180 : -360;
+    const rotateDeg = active ? flipDeg : 0;
 
     const motionDefaultStyle = {
       rotate: rotateDeg,
@@ -93,6 +94,25 @@ export default class IconButton extends React.PureComponent {
       rotate: animate ? spring(rotateDeg, springOpts) : 0,
     };
 
+    if (!animate) {
+      // Perf optimization: avoid unnecessary <Motion> components unless
+      // we actually need to animate.
+      return (
+        <button
+          aria-label={title}
+          aria-pressed={pressed}
+          aria-expanded={expanded}
+          title={title}
+          className={classes}
+          onClick={this.handleClick}
+          style={style}
+          tabIndex={tabIndex}
+        >
+          <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
+        </button>
+      );
+    }
+
     return (
       <Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
         {({ rotate }) =>
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 83cf8b871..5ed46dc93 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -9,6 +9,7 @@ import IconButton from './icon_button';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { isIOS } from '../is_mobile';
 import classNames from 'classnames';
+import { autoPlayGif } from '../initial_state';
 
 const messages = defineMessages({
   toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@@ -26,11 +27,9 @@ class Item extends React.PureComponent {
     index: PropTypes.number.isRequired,
     size: PropTypes.number.isRequired,
     onClick: PropTypes.func.isRequired,
-    autoPlayGif: PropTypes.bool,
   };
 
   static defaultProps = {
-    autoPlayGif: false,
     standalone: false,
     index: 0,
     size: 1,
@@ -50,7 +49,7 @@ class Item extends React.PureComponent {
   }
 
   hoverToPlay () {
-    const { attachment, autoPlayGif } = this.props;
+    const { attachment } = this.props;
     return !autoPlayGif && attachment.get('type') === 'gifv';
   }
 
@@ -142,7 +141,7 @@ class Item extends React.PureComponent {
         </a>
       );
     } else if (attachment.get('type') === 'gifv') {
-      const autoPlay = !isIOS() && this.props.autoPlayGif;
+      const autoPlay = !isIOS() && autoPlayGif;
 
       thumbnail = (
         <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@@ -184,11 +183,9 @@ export default class MediaGallery extends React.PureComponent {
     height: PropTypes.number.isRequired,
     onOpenMedia: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
-    autoPlayGif: PropTypes.bool,
   };
 
   static defaultProps = {
-    autoPlayGif: false,
     standalone: false,
   };
 
@@ -264,9 +261,9 @@ export default class MediaGallery extends React.PureComponent {
       const size = media.take(4).size;
 
       if (this.isStandaloneEligible()) {
-        children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />;
+        children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
       } else {
-        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
+        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
       }
     }
 
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index ab9d48510..71228ca6c 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -1,5 +1,5 @@
 import React, { PureComponent } from 'react';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import PropTypes from 'prop-types';
 import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
 import LoadMore from './load_more';
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index b9be20033..5a01c0cdd 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -39,9 +39,6 @@ export default class Status extends ImmutablePureComponent {
     onBlock: PropTypes.func,
     onEmbed: PropTypes.func,
     onHeightChange: PropTypes.func,
-    me: PropTypes.string,
-    boostModal: PropTypes.bool,
-    autoPlayGif: PropTypes.bool,
     muted: PropTypes.bool,
     hidden: PropTypes.bool,
     onMoveUp: PropTypes.func,
@@ -57,9 +54,6 @@ export default class Status extends ImmutablePureComponent {
   updateOnProps = [
     'status',
     'account',
-    'me',
-    'boostModal',
-    'autoPlayGif',
     'muted',
     'hidden',
   ]
@@ -200,7 +194,7 @@ export default class Status extends ImmutablePureComponent {
       } else {
         media = (
           <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
-            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />}
+            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
           </Bundle>
         );
       }
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index af152cc32..35daf70b9 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -8,6 +8,7 @@ import IconButton from './icon_button';
 import DropdownMenuContainer from '../containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { me } from '../initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -50,7 +51,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onEmbed: PropTypes.func,
     onMuteConversation: PropTypes.func,
     onPin: PropTypes.func,
-    me: PropTypes.string,
     withDismiss: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -59,7 +59,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
   // evaluate to false. See react-immutable-pure-component for usage.
   updateOnProps = [
     'status',
-    'me',
     'withDismiss',
   ]
 
@@ -119,7 +118,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   render () {
-    const { status, me, intl, withDismiss } = this.props;
+    const { status, intl, withDismiss } = this.props;
 
     const mutingConversation = status.get('muted');
     const anonymousAccess    = !me;
diff --git a/app/javascript/mastodon/containers/account_container.js b/app/javascript/mastodon/containers/account_container.js
index 5728c878e..5a5136dd1 100644
--- a/app/javascript/mastodon/containers/account_container.js
+++ b/app/javascript/mastodon/containers/account_container.js
@@ -13,6 +13,7 @@ import {
 } from '../actions/accounts';
 import { openModal } from '../actions/modal';
 import { initMuteModal } from '../actions/mutes';
+import { unfollowModal } from '../initial_state';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -23,8 +24,6 @@ const makeMapStateToProps = () => {
 
   const mapStateToProps = (state, props) => ({
     account: getAccount(state, props.id),
-    me: state.getIn(['meta', 'me']),
-    unfollowModal: state.getIn(['meta', 'unfollow_modal']),
   });
 
   return mapStateToProps;
@@ -34,7 +33,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onFollow (account) {
     if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
-      if (this.unfollowModal) {
+      if (unfollowModal) {
         dispatch(openModal('CONFIRM', {
           message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
           confirm: intl.formatMessage(messages.unfollowConfirm),
diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js
index db452d03a..5ee1d2f14 100644
--- a/app/javascript/mastodon/containers/compose_container.js
+++ b/app/javascript/mastodon/containers/compose_container.js
@@ -6,15 +6,14 @@ import { hydrateStore } from '../actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import Compose from '../features/standalone/compose';
+import initialState from '../initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
 const store = configureStore();
-const initialStateContainer = document.getElementById('initial-state');
 
-if (initialStateContainer !== null) {
-  const initialState = JSON.parse(initialStateContainer.textContent);
+if (initialState) {
   store.dispatch(hydrateStore(initialState));
 }
 
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index a7138e62d..d1710445b 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -4,23 +4,18 @@ import PropTypes from 'prop-types';
 import configureStore from '../store/configureStore';
 import { showOnboardingOnce } from '../actions/onboarding';
 import { BrowserRouter, Route } from 'react-router-dom';
-import { ScrollContext } from 'react-router-scroll';
+import { ScrollContext } from 'react-router-scroll-4';
 import UI from '../features/ui';
 import { hydrateStore } from '../actions/store';
 import { connectUserStream } from '../actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
+import initialState from '../initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
 export const store = configureStore();
-const initialState = JSON.parse(document.getElementById('initial-state').textContent);
-try {
-  initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
-} catch (e) {
-  initialState.local_settings = {};
-}
 const hydrateAction = hydrateStore(initialState);
 store.dispatch(hydrateAction);
 
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index e8821223d..b9c461f31 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -17,20 +17,18 @@ import {
   pin,
   unpin,
 } from '../actions/interactions';
-import {
-  blockAccount,
-  muteAccount,
-} from '../actions/accounts';
+import { blockAccount } from '../actions/accounts';
 import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
+import { initMuteModal } from '../actions/mutes';
 import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { boostModal, deleteModal } from '../initial_state';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
-  muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
 });
 
 const makeMapStateToProps = () => {
@@ -38,10 +36,6 @@ const makeMapStateToProps = () => {
 
   const mapStateToProps = (state, props) => ({
     status: getStatus(state, props.id),
-    me: state.getIn(['meta', 'me']),
-    boostModal: state.getIn(['meta', 'boost_modal']),
-    deleteModal: state.getIn(['meta', 'delete_modal']),
-    autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
   });
 
   return mapStateToProps;
@@ -61,7 +55,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else {
-      if (e.shiftKey || !this.boostModal) {
+      if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
         dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
@@ -90,7 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onDelete (status) {
-    if (!this.deleteModal) {
+    if (!deleteModal) {
       dispatch(deleteStatus(status.get('id')));
     } else {
       dispatch(openModal('CONFIRM', {
@@ -126,11 +120,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onMute (account) {
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.muteConfirm),
-      onConfirm: () => dispatch(muteAccount(account.get('id'))),
-    }));
+    dispatch(initMuteModal(account));
   },
 
   onMuteConversation (status) {
diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js
index 4be037955..e84c921ee 100644
--- a/app/javascript/mastodon/containers/timeline_container.js
+++ b/app/javascript/mastodon/containers/timeline_container.js
@@ -7,15 +7,14 @@ import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import PublicTimeline from '../features/standalone/public_timeline';
 import HashtagTimeline from '../features/standalone/hashtag_timeline';
+import initialState from '../initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
 const store = configureStore();
-const initialStateContainer = document.getElementById('initial-state');
 
-if (initialStateContainer !== null) {
-  const initialState = JSON.parse(initialStateContainer.textContent);
+if (initialState) {
   store.dispatch(hydrateStore(initialState));
 }
 
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index 718e7fbad..389296c42 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
 import { Link } from 'react-router-dom';
 import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
+import { me } from '../../../initial_state';
 
 const messages = defineMessages({
   mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -28,7 +29,6 @@ export default class ActionBar extends React.PureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
@@ -47,7 +47,7 @@ export default class ActionBar extends React.PureComponent {
   }
 
   render () {
-    const { account, me, intl } = this.props;
+    const { account, intl } = this.props;
 
     let menu = [];
     let extraInfo = '';
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 57678d162..b3a73a590 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -8,8 +8,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 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';
+import { autoPlayGif, me } from '../../../initial_state';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -17,19 +17,10 @@ const messages = defineMessages({
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
 });
 
-const makeMapStateToProps = () => {
-  const mapStateToProps = state => ({
-    autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
-  });
-
-  return mapStateToProps;
-};
-
 class Avatar extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    autoPlayGif: PropTypes.bool.isRequired,
   };
 
   state = {
@@ -47,7 +38,7 @@ class Avatar extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, autoPlayGif }   = this.props;
+    const { account }   = this.props;
     const { isHovered } = this.state;
 
     return (
@@ -74,20 +65,17 @@ class Avatar extends ImmutablePureComponent {
 
 }
 
-@connect(makeMapStateToProps)
 @injectIntl
 export default class Header extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map,
-    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
-    autoPlayGif: PropTypes.bool.isRequired,
   };
 
   render () {
-    const { account, me, intl } = this.props;
+    const { account, intl } = this.props;
 
     if (!account) {
       return null;
@@ -127,7 +115,7 @@ export default class Header extends ImmutablePureComponent {
     return (
       <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
         <div>
-          <Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
+          <Avatar account={account} />
 
           <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
           <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index 2a88addc4..a40722417 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -12,14 +12,13 @@ import { getAccountGallery } from '../../selectors';
 import MediaItem from './components/media_item';
 import HeaderContainer from '../account_timeline/containers/header_container';
 import { FormattedMessage } from 'react-intl';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import LoadMore from '../../components/load_more';
 
 const mapStateToProps = (state, props) => ({
   medias: getAccountGallery(state, props.params.accountId),
   isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
   hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
-  autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
 });
 
 @connect(mapStateToProps)
@@ -31,7 +30,6 @@ export default class AccountGallery extends ImmutablePureComponent {
     medias: ImmutablePropTypes.list.isRequired,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
-    autoPlayGif: PropTypes.bool,
   };
 
   componentDidMount () {
@@ -67,7 +65,7 @@ export default class AccountGallery extends ImmutablePureComponent {
   }
 
   render () {
-    const { medias, autoPlayGif, isLoading, hasMore } = this.props;
+    const { medias, isLoading, hasMore } = this.props;
 
     let loadMore = null;
 
@@ -100,7 +98,6 @@ export default class AccountGallery extends ImmutablePureComponent {
                 <MediaItem
                   key={media.get('id')}
                   media={media}
-                  autoPlayGif={autoPlayGif}
                 />
               )}
               {loadMore}
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index b33df282f..9a087e922 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -10,7 +10,6 @@ export default class Header extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map,
-    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
@@ -66,7 +65,7 @@ export default class Header extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, me } = this.props;
+    const { account } = this.props;
 
     if (account === null) {
       return <MissingIndicator />;
@@ -76,13 +75,11 @@ export default class Header extends ImmutablePureComponent {
       <div className='account-timeline__header'>
         <InnerHeader
           account={account}
-          me={me}
           onFollow={this.handleFollow}
         />
 
         <ActionBar
           account={account}
-          me={me}
           onBlock={this.handleBlock}
           onMention={this.handleMention}
           onReblogToggle={this.handleReblogToggle}
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index 68c037e9b..b41eb19d4 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -15,6 +15,7 @@ import { initReport } from '../../../actions/reports';
 import { openModal } from '../../../actions/modal';
 import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { unfollowModal } from '../../../initial_state';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -27,8 +28,6 @@ const makeMapStateToProps = () => {
 
   const mapStateToProps = (state, { accountId }) => ({
     account: getAccount(state, accountId),
-    me: state.getIn(['meta', 'me']),
-    unfollowModal: state.getIn(['meta', 'unfollow_modal']),
   });
 
   return mapStateToProps;
@@ -38,7 +37,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onFollow (account) {
     if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
-      if (this.unfollowModal) {
+      if (unfollowModal) {
         dispatch(openModal('CONFIRM', {
           message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
           confirm: intl.formatMessage(messages.unfollowConfirm),
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index e3b864aee..3ad370e32 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -16,7 +16,6 @@ const mapStateToProps = (state, props) => ({
   statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
   isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
   hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
-  me: state.getIn(['meta', 'me']),
 });
 
 @connect(mapStateToProps)
@@ -28,7 +27,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
     statusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
-    me: PropTypes.string.isRequired,
   };
 
   componentWillMount () {
@@ -50,7 +48,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
   }
 
   render () {
-    const { statusIds, isLoading, hasMore, me } = this.props;
+    const { statusIds, isLoading, hasMore } = this.props;
 
     if (!statusIds && isLoading) {
       return (
@@ -70,7 +68,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
           statusIds={statusIds}
           isLoading={isLoading}
           hasMore={hasMore}
-          me={me}
           onScrollToBottom={this.handleScrollToBottom}
         />
       </Column>
diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js
index e73d984a9..9199529dd 100644
--- a/app/javascript/mastodon/features/blocks/index.js
+++ b/app/javascript/mastodon/features/blocks/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import LoadingIndicator from '../../components/loading_indicator';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import Column from '../ui/components/column';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import AccountContainer from '../../containers/account_container';
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 2da656fc0..aaca45493 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -19,6 +19,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { length } from 'stringz';
 import { countableText } from '../util/counter';
 import ComposeAttachOptions from '../../../../glitch/components/compose/attach_options/index';
+import initialState from '../../../initial_state';
+
+const maxChars = initialState.max_toot_chars;
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -45,7 +48,6 @@ export default class ComposeForm extends ImmutablePureComponent {
     preselectDate: PropTypes.instanceOf(Date),
     is_submitting: PropTypes.bool,
     is_uploading: PropTypes.bool,
-    me: PropTypes.string,
     onChange: PropTypes.func.isRequired,
     onSubmit: PropTypes.func.isRequired,
     onClearSuggestions: PropTypes.func.isRequired,
@@ -206,7 +208,7 @@ export default class ComposeForm extends ImmutablePureComponent {
       }
     }
 
-    const submitDisabled = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0);
+    const submitDisabled = disabled || this.props.is_uploading || length(text) > maxChars || (text.length !== 0 && text.trim().length === 0);
 
     return (
       <div className='compose-form'>
@@ -256,7 +258,7 @@ export default class ComposeForm extends ImmutablePureComponent {
         </div>
 
         <div className='compose-form__publish'>
-          <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
+          <div className='character-counter__wrapper'><CharacterCounter max={maxChars} text={text} /></div>
           <div className='compose-form__publish-button-wrapper'>
             {
               showSideArm ?
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index dffa04ff0..dc8fc02ba 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -157,7 +157,6 @@ class EmojiPickerMenu extends React.PureComponent {
     intl: PropTypes.object.isRequired,
     skinTone: PropTypes.number.isRequired,
     onSkinTone: PropTypes.func.isRequired,
-    autoPlay: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -235,7 +234,7 @@ class EmojiPickerMenu extends React.PureComponent {
   }
 
   render () {
-    const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props;
+    const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
 
     if (loading) {
       return <div style={{ width: 299 }} />;
@@ -250,7 +249,7 @@ class EmojiPickerMenu extends React.PureComponent {
           perLine={8}
           emojiSize={22}
           sheetSize={32}
-          custom={buildCustomEmojis(custom_emojis, autoPlay)}
+          custom={buildCustomEmojis(custom_emojis)}
           color=''
           emoji=''
           set='twitter'
@@ -284,7 +283,6 @@ export default class EmojiPickerDropdown extends React.PureComponent {
   static propTypes = {
     custom_emojis: ImmutablePropTypes.list,
     frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
-    autoPlay: PropTypes.bool,
     intl: PropTypes.object.isRequired,
     onPickEmoji: PropTypes.func.isRequired,
     onSkinTone: PropTypes.func.isRequired,
@@ -346,7 +344,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
   }
 
   render () {
-    const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
+    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
     const title = intl.formatMessage(messages.emoji);
     const { active, loading } = this.state;
 
@@ -366,7 +364,6 @@ export default class EmojiPickerDropdown extends React.PureComponent {
             loading={loading}
             onClose={this.onHideDropdown}
             onPick={onPickEmoji}
-            autoPlay={autoPlay}
             onSkinTone={onSkinTone}
             skinTone={skinTone}
             frequentlyUsedEmojis={frequentlyUsedEmojis}
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
index ffa0a3442..dfe8241c6 100644
--- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js
+++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
@@ -23,7 +23,6 @@ const mapStateToProps = state => ({
   preselectDate: state.getIn(['compose', 'preselectDate']),
   is_submitting: state.getIn(['compose', 'is_submitting']),
   is_uploading: state.getIn(['compose', 'is_uploading']),
-  me: state.getIn(['compose', 'me']),
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
   settings: state.get('local_settings'),
   filesAttached: state.getIn(['compose', 'media_attachments']).size > 0,
diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
index 699687c69..e6a535a5d 100644
--- a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
@@ -61,7 +61,6 @@ const getCustomEmojis = createSelector([
 
 const mapStateToProps = state => ({
   custom_emojis: getCustomEmojis(state),
-  autoPlay: state.getIn(['meta', 'auto_play_gif']),
   skinTone: state.getIn(['settings', 'skinTone']),
   frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
 });
diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js
index 8cc53c087..eb9f3ea45 100644
--- a/app/javascript/mastodon/features/compose/containers/navigation_container.js
+++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js
@@ -1,9 +1,10 @@
 import { connect }   from 'react-redux';
 import NavigationBar from '../components/navigation_bar';
+import { me } from '../../../initial_state';
 
 const mapStateToProps = state => {
   return {
-    account: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+    account: state.getIn(['accounts', me]),
   };
 };
 
diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js
index 35eab5976..d34471a3e 100644
--- a/app/javascript/mastodon/features/compose/containers/warning_container.js
+++ b/app/javascript/mastodon/features/compose/containers/warning_container.js
@@ -3,9 +3,10 @@ import { connect } from 'react-redux';
 import Warning from '../components/warning';
 import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
+import { me } from '../../../initial_state';
 
 const mapStateToProps = state => ({
-  needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']),
+  needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
 });
 
 const WarningWrapper = ({ needsLockWarning }) => {
diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js
index e6d2487c5..700ba2163 100644
--- a/app/javascript/mastodon/features/compose/util/counter.js
+++ b/app/javascript/mastodon/features/compose/util/counter.js
@@ -5,5 +5,5 @@ const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
 export function countableText(inputText) {
   return inputText
     .replace(urlRegex, urlPlaceholder)
-    .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '@$2');
+    .replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3');
 };
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
index 636402172..372459c78 100644
--- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -57,5 +57,21 @@ describe('emoji', () => {
     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" />');
     });
+
+    it('avoid emojifying on invisible text', () => {
+      expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'))
+        .toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>');
+      expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } }))
+        .toEqual('<span class="invisible">:luigi:</span>');
+    });
+
+    it('avoid emojifying on invisible text with nested tags', () => {
+      expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+      expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+      expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+    });
   });
 });
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index b70fc2b37..0f005dd50 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -1,3 +1,4 @@
+import { autoPlayGif } from '../../initial_state';
 import unicodeMapping from './emoji_unicode_mapping_light';
 import Trie from 'substring-trie';
 
@@ -5,13 +6,13 @@ const trie = new Trie(Object.keys(unicodeMapping));
 
 const assetHost = process.env.CDN_HOST || '';
 
-let allowAnimations = false;
-
 const emojify = (str, customEmojis = {}) => {
-  let rtn = '';
+  const tagCharsWithoutEmojis = '<&';
+  const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
+  let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
   for (;;) {
     let match, i = 0, tag;
-    while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) {
+    while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
       i += str.codePointAt(i) < 65536 ? 1 : 2;
     }
     let rend, replacement = '';
@@ -27,7 +28,7 @@ const emojify = (str, customEmojis = {}) => {
         // now got a replacee as ':shortname:'
         // if you want additional emoji handler, add statements below which set replacement and return true.
         if (shortname in customEmojis) {
-          const filename = allowAnimations ? customEmojis[shortname].url : customEmojis[shortname].static_url;
+          const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
           replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
           return true;
         }
@@ -35,7 +36,26 @@ const emojify = (str, customEmojis = {}) => {
       })()) rend = ++i;
     } else if (tag >= 0) { // <, &
       rend = str.indexOf('>;'[tag], i + 1) + 1;
-      if (!rend) break;
+      if (!rend) {
+        break;
+      }
+      if (tag === 0) {
+        if (invisible) {
+          if (str[i + 1] === '/') { // closing tag
+            if (!--invisible) {
+              tagChars = tagCharsWithEmojis;
+            }
+          } else if (str[rend - 2] !== '/') { // opening tag
+            invisible++;
+          }
+        } else {
+          if (str.startsWith('<span class="invisible">', i)) {
+            // avoid emojifying on invisible text
+            invisible = 1;
+            tagChars = tagCharsWithoutEmojis;
+          }
+        }
+      }
       i = rend;
     } else { // matched to unicode emoji
       const { filename, shortCode } = unicodeMapping[match];
@@ -51,14 +71,12 @@ const emojify = (str, customEmojis = {}) => {
 
 export default emojify;
 
-export const buildCustomEmojis = (customEmojis, overrideAllowAnimations = false) => {
+export const buildCustomEmojis = (customEmojis) => {
   const emojis = [];
 
-  allowAnimations = overrideAllowAnimations;
-
   customEmojis.forEach(emoji => {
     const shortcode = emoji.get('shortcode');
-    const url       = allowAnimations ? emoji.get('url') : emoji.get('static_url');
+    const url       = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
     const name      = shortcode.replace(':', '');
 
     emojis.push({
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js
index c0cba952a..e5b834a74 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.js
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js
@@ -64,14 +64,14 @@ Object.keys(emojiMap).forEach(key => {
 
 Object.keys(emojiIndex.emojis).forEach(key => {
   const { native } = emojiIndex.emojis[key];
-  const { short_names, search, unified } = emojiMartData.emojis[key];
+  let { short_names, search, unified } = emojiMartData.emojis[key];
   if (short_names[0] !== key) {
     throw new Error('The compresser expects the first short_code to be the ' +
       'key. It may need to be rewritten if the emoji change such that this ' +
       'is no longer the case.');
   }
 
-  short_names.splice(0, 1); // first short name can be inferred from the key
+  short_names = short_names.slice(1); // first short name can be inferred from the key
 
   const searchData = [native, short_names, search];
   if (unicodeToUnifiedName(native) !== unified) {
diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js
index 4dbfefd87..6f113beb4 100644
--- a/app/javascript/mastodon/features/favourites/index.js
+++ b/app/javascript/mastodon/features/favourites/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import LoadingIndicator from '../../components/loading_indicator';
 import { fetchFavourites } from '../../actions/interactions';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import AccountContainer from '../../containers/account_container';
 import Column from '../ui/components/column';
 import ColumnBackButton from '../../components/column_back_button';
diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js
index 94109b151..1fa52d511 100644
--- a/app/javascript/mastodon/features/follow_requests/index.js
+++ b/app/javascript/mastodon/features/follow_requests/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import LoadingIndicator from '../../components/loading_indicator';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import Column from '../ui/components/column';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import AccountAuthorizeContainer from './containers/account_authorize_container';
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index 89445559f..f64ed7948 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -8,7 +8,7 @@ import {
   fetchFollowers,
   expandFollowers,
 } from '../../actions/accounts';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import AccountContainer from '../../containers/account_container';
 import Column from '../ui/components/column';
 import HeaderContainer from '../account_timeline/containers/header_container';
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index c34830276..a0c0fac05 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -8,7 +8,7 @@ import {
   fetchFollowing,
   expandFollowing,
 } from '../../actions/accounts';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import AccountContainer from '../../containers/account_container';
 import Column from '../ui/components/column';
 import HeaderContainer from '../account_timeline/containers/header_container';
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 9b94b9830..2f7d9281e 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -8,6 +8,7 @@ import { openModal } from '../../actions/modal';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { me } from '../../initial_state';
 
 const messages = defineMessages({
   heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -31,7 +32,7 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = state => ({
-  me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+  myAccount: state.getIn(['accounts', me]),
   columns: state.getIn(['settings', 'columns']),
 });
 
@@ -41,7 +42,7 @@ export default class GettingStarted extends ImmutablePureComponent {
 
   static propTypes = {
     intl: PropTypes.object.isRequired,
-    me: ImmutablePropTypes.map.isRequired,
+    myAccount: ImmutablePropTypes.map.isRequired,
     columns: ImmutablePropTypes.list,
     multiColumn: PropTypes.bool,
     dispatch: PropTypes.func.isRequired,
@@ -57,7 +58,7 @@ export default class GettingStarted extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, me, columns, multiColumn } = this.props;
+    const { intl, myAccount, columns, multiColumn } = this.props;
 
     let navItems = [];
 
@@ -88,7 +89,7 @@ export default class GettingStarted extends ImmutablePureComponent {
       <ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
     ]);
 
-    if (me.get('locked')) {
+    if (myAccount.get('locked')) {
       navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
     }
 
diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js
index 0f3b8e710..ae6ec343f 100644
--- a/app/javascript/mastodon/features/mutes/index.js
+++ b/app/javascript/mastodon/features/mutes/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import LoadingIndicator from '../../components/loading_indicator';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import Column from '../ui/components/column';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import AccountContainer from '../../containers/account_container';
diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js
index f1904786a..579d6aaa0 100644
--- a/app/javascript/mastodon/features/reblogs/index.js
+++ b/app/javascript/mastodon/features/reblogs/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import LoadingIndicator from '../../components/loading_indicator';
 import { fetchReblogs } from '../../actions/interactions';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import AccountContainer from '../../containers/account_container';
 import Column from '../ui/components/column';
 import ColumnBackButton from '../../components/column_back_button';
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index 3e94f7446..8c6994a07 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -4,6 +4,7 @@ import IconButton from '../../../components/icon_button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
+import { me } from '../../../initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -36,7 +37,6 @@ export default class ActionBar extends React.PureComponent {
     onReport: PropTypes.func,
     onPin: PropTypes.func,
     onEmbed: PropTypes.func,
-    me: PropTypes.string.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
@@ -80,7 +80,7 @@ export default class ActionBar extends React.PureComponent {
   }
 
   render () {
-    const { status, me, intl } = this.props;
+    const { status, intl } = this.props;
 
     const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
 
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index d8547db36..85a030ea8 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -25,7 +25,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
     settings: ImmutablePropTypes.map.isRequired,
     onOpenMedia: PropTypes.func.isRequired,
     onOpenVideo: PropTypes.func.isRequired,
-    autoPlayGif: PropTypes.bool,
   };
 
   handleAccountClick = (e) => {
@@ -76,7 +75,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
             fullwidth={settings.getIn(['media', 'fullwidth'])}
             height={250}
             onOpenMedia={this.props.onOpenMedia}
-            autoPlayGif={this.props.autoPlayGif}
           />
         );
         mediaIcon = 'picture-o';
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index c40630a0a..e7ea046dd 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
+import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { fetchStatus } from '../../actions/statuses';
 import MissingIndicator from '../../components/missing_indicator';
@@ -22,13 +23,15 @@ import {
 import { deleteStatus } from '../../actions/statuses';
 import { initReport } from '../../actions/reports';
 import { makeGetStatus } from '../../selectors';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
 import ColumnBackButton from '../../components/column_back_button';
 import StatusContainer from '../../../glitch/components/status/container';
 import { openModal } from '../../actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
+import { boostModal, deleteModal } from '../../initial_state';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -43,10 +46,6 @@ const makeMapStateToProps = () => {
     settings: state.get('local_settings'),
     ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
     descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
-    me: state.getIn(['meta', 'me']),
-    boostModal: state.getIn(['meta', 'boost_modal']),
-    deleteModal: state.getIn(['meta', 'delete_modal']),
-    autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
   });
 
   return mapStateToProps;
@@ -67,17 +66,21 @@ export default class Status extends ImmutablePureComponent {
     settings: ImmutablePropTypes.map.isRequired,
     ancestorsIds: ImmutablePropTypes.list,
     descendantsIds: ImmutablePropTypes.list,
-    me: PropTypes.string,
-    boostModal: PropTypes.bool,
-    deleteModal: PropTypes.bool,
-    autoPlayGif: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
+  state = {
+    fullscreen: false,
+  };
+
   componentWillMount () {
     this.props.dispatch(fetchStatus(this.props.params.statusId));
   }
 
+  componentDidMount () {
+    attachFullscreenListener(this.onFullScreenChange);
+  }
+
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
       this._scrolledIntoView = false;
@@ -113,7 +116,7 @@ export default class Status extends ImmutablePureComponent {
     if (status.get('reblogged')) {
       this.props.dispatch(unreblog(status));
     } else {
-      if (e.shiftKey || !this.props.boostModal) {
+      if (e.shiftKey || !boostModal) {
         this.handleModalReblog(status);
       } else {
         this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
@@ -124,7 +127,7 @@ export default class Status extends ImmutablePureComponent {
   handleDeleteClick = (status) => {
     const { dispatch, intl } = this.props;
 
-    if (!this.props.deleteModal) {
+    if (!deleteModal) {
       dispatch(deleteStatus(status.get('id')));
     } else {
       dispatch(openModal('CONFIRM', {
@@ -259,9 +262,18 @@ export default class Status extends ImmutablePureComponent {
     }
   }
 
+  componentWillUnmount () {
+    detachFullscreenListener(this.onFullScreenChange);
+  }
+
+  onFullScreenChange = () => {
+    this.setState({ fullscreen: isFullscreen() });
+  }
+
   render () {
     let ancestors, descendants;
-    const { status, settings, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
+    const { status, settings, ancestorsIds, descendantsIds } = this.props;
+    const { fullscreen } = this.state;
 
     if (status === null) {
       return (
@@ -295,7 +307,7 @@ export default class Status extends ImmutablePureComponent {
         <ColumnBackButton />
 
         <ScrollContainer scrollKey='thread'>
-          <div className='scrollable detailed-status__wrapper' ref={this.setRef}>
+          <div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}>
             {ancestors}
 
             <HotKeys handlers={handlers}>
@@ -303,15 +315,12 @@ export default class Status extends ImmutablePureComponent {
                 <DetailedStatus
                   status={status}
                   settings={settings}
-                  autoPlayGif={autoPlayGif}
-                  me={me}
                   onOpenVideo={this.handleOpenVideo}
                   onOpenMedia={this.handleOpenMedia}
                 />
 
                 <ActionBar
                   status={status}
-                  me={me}
                   onReply={this.handleReplyClick}
                   onFavourite={this.handleFavouriteClick}
                   onReblog={this.handleReblogClick}
diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js
index b5e83bb71..73e48cf09 100644
--- a/app/javascript/mastodon/features/ui/components/mute_modal.js
+++ b/app/javascript/mastodon/features/ui/components/mute_modal.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import { injectIntl, FormattedMessage } from 'react-intl';
+import Toggle from 'react-toggle';
 import Button from '../../../components/button';
 import { closeModal } from '../../../actions/modal';
 import { muteAccount } from '../../../actions/accounts';
@@ -80,12 +81,13 @@ export default class MuteModal extends React.PureComponent {
               values={{ name: <strong>@{account.get('acct')}</strong> }}
             />
           </p>
-          <p>
+          <div>
             <label htmlFor='mute-modal__hide-notifications-checkbox'>
               <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
-              <input id='mute-modal__hide-notifications-checkbox' type='checkbox' checked={notifications} onChange={this.toggleNotifications} />
+              {' '}
+              <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
             </label>
-          </p>
+          </div>
         </div>
 
         <div className='mute-modal__action-bar'>
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
index daf6b485c..1f9f0cd03 100644
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -14,6 +14,7 @@ import {
   List as ImmutableList,
   Map as ImmutableMap,
 } from 'immutable';
+import { me } from '../../../initial_state';
 
 const noop = () => { };
 
@@ -43,11 +44,11 @@ PageOne.propTypes = {
   domain: PropTypes.string.isRequired,
 };
 
-const PageTwo = ({ me }) => (
+const PageTwo = ({ myAccount }) => (
   <div className='onboarding-modal__page onboarding-modal__page-two'>
     <div className='figure non-interactive'>
       <div className='pseudo-drawer'>
-        <NavigationBar onClose={noop} account={me} />
+        <NavigationBar onClose={noop} account={myAccount} />
       </div>
       <ComposeForm
         text='Awoo! #introductions'
@@ -73,10 +74,10 @@ const PageTwo = ({ me }) => (
 );
 
 PageTwo.propTypes = {
-  me: ImmutablePropTypes.map.isRequired,
+  myAccount: ImmutablePropTypes.map.isRequired,
 };
 
-const PageThree = ({ me }) => (
+const PageThree = ({ myAccount }) => (
   <div className='onboarding-modal__page onboarding-modal__page-three'>
     <div className='figure non-interactive'>
       <Search
@@ -88,7 +89,7 @@ const PageThree = ({ me }) => (
       />
 
       <div className='pseudo-drawer'>
-        <NavigationBar onClose={noop} account={me} />
+        <NavigationBar onClose={noop} account={myAccount} />
       </div>
     </div>
 
@@ -98,7 +99,7 @@ const PageThree = ({ me }) => (
 );
 
 PageThree.propTypes = {
-  me: ImmutablePropTypes.map.isRequired,
+  myAccount: ImmutablePropTypes.map.isRequired,
 };
 
 const PageFour = ({ domain, intl }) => (
@@ -166,7 +167,7 @@ PageSix.propTypes = {
 };
 
 const mapStateToProps = state => ({
-  me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+  myAccount: state.getIn(['accounts', me]),
   admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
   domain: state.getIn(['meta', 'domain']),
 });
@@ -178,7 +179,7 @@ export default class OnboardingModal extends React.PureComponent {
   static propTypes = {
     onClose: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
-    me: ImmutablePropTypes.map.isRequired,
+    myAccount: ImmutablePropTypes.map.isRequired,
     domain: PropTypes.string.isRequired,
     admin: ImmutablePropTypes.map,
   };
@@ -188,11 +189,11 @@ export default class OnboardingModal extends React.PureComponent {
   };
 
   componentWillMount() {
-    const { me, admin, domain, intl } = this.props;
+    const { myAccount, admin, domain, intl } = this.props;
     this.pages = [
-      <PageOne acct={me.get('acct')} domain={domain} />,
-      <PageTwo me={me} />,
-      <PageThree me={me} />,
+      <PageOne acct={myAccount.get('acct')} domain={domain} />,
+      <PageTwo myAccount={myAccount} />,
+      <PageThree myAccount={myAccount} />,
       <PageFour domain={domain} intl={intl} />,
       <PageSix admin={admin} domain={domain} />,
     ];
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index ff29bfdd4..a0aec4403 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -4,13 +4,13 @@ import { scrollTopTimeline } from '../../../actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { debounce } from 'lodash';
+import { me } from '../../../initial_state';
 
 const makeGetStatusIds = () => createSelector([
   (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
   (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
   (state)           => state.get('statuses'),
-  (state)           => state.getIn(['meta', 'me']),
-], (columnSettings, statusIds, statuses, me) => {
+], (columnSettings, statusIds, statuses) => {
   const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
   let regex      = null;
 
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 9f77ab5aa..69eb1bbf7 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -40,18 +40,23 @@ import {
   PinnedStatuses,
 } from './util/async-components';
 import { HotKeys } from 'react-hotkeys';
+import { me } from '../../initial_state';
+import { defineMessages, injectIntl } from 'react-intl';
 
 // Dummy import, to make sure that <Status /> ends up in the application bundle.
 // Without this it ends up in ~8 very commonly used bundles.
 import '../../../glitch/components/status';
 
+const messages = defineMessages({
+  beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
+});
+
 const mapStateToProps = state => ({
-  systemFontUi: state.getIn(['meta', 'system_font_ui']),
+  isComposing: state.getIn(['compose', 'is_composing']),
+  hasComposingText: state.getIn(['compose', 'text']) !== '',
   layout: state.getIn(['local_settings', 'layout']),
   isWide: state.getIn(['local_settings', 'stretch']),
   navbarUnder: state.getIn(['local_settings', 'navbar_under']),
-  me: state.getIn(['meta', 'me']),
-  isComposing: state.getIn(['compose', 'is_composing']),
 });
 
 const keyMap = {
@@ -82,6 +87,7 @@ const keyMap = {
 };
 
 @connect(mapStateToProps)
+@injectIntl
 @withRouter
 export default class UI extends React.Component {
 
@@ -97,8 +103,9 @@ export default class UI extends React.Component {
     systemFontUi: PropTypes.bool,
     navbarUnder: PropTypes.bool,
     isComposing: PropTypes.bool,
-    me: PropTypes.string,
+    hasComposingText: PropTypes.bool,
     location: PropTypes.object,
+    intl: PropTypes.object.isRequired,
   };
 
   state = {
@@ -106,6 +113,17 @@ export default class UI extends React.Component {
     draggingOver: false,
   };
 
+  handleBeforeUnload = (e) => {
+    const { intl, isComposing, hasComposingText } = this.props;
+
+    if (isComposing && hasComposingText) {
+      // Setting returnValue to any string causes confirmation dialog.
+      // Many browsers no longer display this text to users,
+      // but we set user-friendly message for other browsers, e.g. Edge.
+      e.returnValue = intl.formatMessage(messages.beforeUnload);
+    }
+  }
+
   handleResize = debounce(() => {
     // The cached heights are no longer accurate, invalidate
     this.props.dispatch(clearHeight());
@@ -180,6 +198,7 @@ export default class UI extends React.Component {
   }
 
   componentWillMount () {
+    window.addEventListener('beforeunload', this.handleBeforeUnload, false);
     window.addEventListener('resize', this.handleResize, { passive: true });
     document.addEventListener('dragenter', this.handleDragEnter, false);
     document.addEventListener('dragover', this.handleDragOver, false);
@@ -222,6 +241,7 @@ export default class UI extends React.Component {
   }
 
   componentWillUnmount () {
+    window.removeEventListener('beforeunload', this.handleBeforeUnload);
     window.removeEventListener('resize', this.handleResize);
     document.removeEventListener('dragenter', this.handleDragEnter);
     document.removeEventListener('dragover', this.handleDragOver);
@@ -321,7 +341,7 @@ export default class UI extends React.Component {
   }
 
   handleHotkeyGoToProfile = () => {
-    this.context.router.history.push(`/accounts/${this.props.me}`);
+    this.context.router.history.push(`/accounts/${me}`);
   }
 
   handleHotkeyGoToBlocked = () => {
diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js
index af6368738..df3a8b54a 100644
--- a/app/javascript/mastodon/features/ui/util/optional_motion.js
+++ b/app/javascript/mastodon/features/ui/util/optional_motion.js
@@ -1,56 +1,5 @@
-// 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 { reduceMotion } from '../../../initial_state';
+import ReducedMotion from './reduced_motion';
 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;
+export default reduceMotion ? ReducedMotion : Motion;
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
index 86b30d488..43007ddc3 100644
--- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -7,11 +7,19 @@ import BundleColumnError from '../components/bundle_column_error';
 import BundleContainer from '../containers/bundle_container';
 
 // Small wrapper to pass multiColumn to the route components
-export const WrappedSwitch = ({ multiColumn, children }) => (
-  <Switch>
-    {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
-  </Switch>
-);
+export class WrappedSwitch extends React.PureComponent {
+
+  render () {
+    const { multiColumn, children } = this.props;
+
+    return (
+      <Switch>
+        {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
+      </Switch>
+    );
+  }
+
+}
 
 WrappedSwitch.propTypes = {
   multiColumn: PropTypes.bool,
diff --git a/app/javascript/mastodon/features/ui/util/reduced_motion.js b/app/javascript/mastodon/features/ui/util/reduced_motion.js
new file mode 100644
index 000000000..95519042b
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/reduced_motion.js
@@ -0,0 +1,44 @@
+// Like react-motion's Motion, but reduces all animations to cross-fades
+// for the benefit of users with motion sickness.
+import React from 'react';
+import Motion from 'react-motion/lib/Motion';
+import PropTypes from 'prop-types';
+
+const stylesToKeep = ['opacity', 'backgroundOpacity'];
+
+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 ReducedMotion extends React.Component {
+
+  static propTypes = {
+    defaultStyle: PropTypes.object,
+    style: PropTypes.object,
+    children: PropTypes.func,
+  }
+
+  render() {
+
+    const { style, defaultStyle, children } = this.props;
+
+    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 ReducedMotion;
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
new file mode 100644
index 000000000..ef5d8b0ef
--- /dev/null
+++ b/app/javascript/mastodon/initial_state.js
@@ -0,0 +1,21 @@
+const element = document.getElementById('initial-state');
+const initialState = element && function () {
+  const result = JSON.parse(element.textContent);
+  try {
+    result.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
+  } catch (e) {
+    result.local_settings = {};
+  }
+  return result;
+}();
+
+const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
+
+export const reduceMotion = getMeta('reduce_motion');
+export const autoPlayGif = getMeta('auto_play_gif');
+export const unfollowModal = getMeta('unfollow_modal');
+export const boostModal = getMeta('boost_modal');
+export const deleteModal = getMeta('delete_modal');
+export const me = getMeta('me');
+
+export default initialState;
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index d99dacd59..2919928af 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -63,7 +63,7 @@
   "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
+  "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.",
   "embed.preview": "다음과 같이 표시됩니다:",
   "emoji_button.activity": "활동",
   "emoji_button.custom": "Custom",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 1e0849d95..d826423b5 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -31,7 +31,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
   "column.home": "Acuèlh",
-  "column.mutes": "Personas en silenci",
+  "column.mutes": "Personas rescondudas",
   "column.notifications": "Notificacions",
   "column.pins": "Tuts penjats",
   "column.public": "Flux public global",
@@ -55,12 +55,12 @@
   "confirmation_modal.cancel": "Anullar",
   "confirmations.block.confirm": "Blocar",
   "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
-  "confirmations.delete.confirm": "Suprimir",
-  "confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?",
+  "confirmations.delete.confirm": "Escafar",
+  "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
   "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
-  "confirmations.mute.confirm": "Metre en silenci",
-  "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
+  "confirmations.mute.confirm": "Rescondre",
+  "confirmations.mute.message": "Sètz segur de voler rescondre {name} ?",
   "confirmations.unfollow.confirm": "Quitar de sègre",
   "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
   "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
@@ -135,7 +135,7 @@
   "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
   "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
   "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
-  "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.",
+  "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.",
   "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
   "onboarding.page_one.welcome": "Benvengut a Mastodon !",
   "onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.",
@@ -159,11 +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.days": "fa {number} d",
+  "relative_time.hours": "fa {number} h",
   "relative_time.just_now": "ara",
-  "relative_time.minutes": "fa {number} minutas",
-  "relative_time.seconds": "fa {number} segondas",
+  "relative_time.minutes": "fa {number} min",
+  "relative_time.seconds": "fa {number} s",
   "reply_indicator.cancel": "Anullar",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Mandar",
@@ -197,7 +197,7 @@
   "status.share": "Partejar",
   "status.show_less": "Tornar plegar",
   "status.show_more": "Desplegar",
-  "status.unmute_conversation": "Conversacions amb silenci levat",
+  "status.unmute_conversation": "Tornar mostrar la conversacion",
   "status.unpin": "Tirar del perfil",
   "tabs_bar.compose": "Compausar",
   "tabs_bar.federated_timeline": "Flux public global",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index cf76f1b1f..b23a5e69f 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -89,7 +89,7 @@
   "follow_request.reject": "Odrzuć",
   "getting_started.appsshort": "Aplikacje",
   "getting_started.faq": "FAQ",
-  "getting_started.heading": "Naucz się korzystać",
+  "getting_started.heading": "Rozpocznij",
   "getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
   "getting_started.userguide": "Podręcznik użytkownika",
   "home.column_settings.advanced": "Zaawansowane",
@@ -174,7 +174,7 @@
   "search_popout.tips.status": "wpis",
   "search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
   "search_popout.tips.user": "użytkownik",
-  "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
+  "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
   "standalone.public_title": "Spojrzenie w głąb…",
   "status.cannot_reblog": "Ten wpis nie może zostać podbity",
   "status.delete": "Usuń",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index ddb8b83f5..a04d1cc31 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -161,7 +161,7 @@
   "privacy.unlisted.short": "Não listada",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "agora",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 827c815cf..bbdf34d2f 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,25 +1,27 @@
 {
   "account.block": "屏蔽 @{name}",
-  "account.block_domain": "隐藏一切来自 {domain} 的嘟文",
-  "account.disclaimer_full": "下列资料不一定完整。",
+  "account.block_domain": "隐藏来自 {domain} 的内容",
+  "account.disclaimer_full": "此处显示的信息可能不是全部内容。",
   "account.edit_profile": "修改个人资料",
   "account.follow": "关注",
   "account.followers": "关注者",
-  "account.follows": "正关注",
-  "account.follows_you": "关注你",
+  "account.follows": "正在关注",
+  "account.follows_you": "关注了你",
   "account.media": "媒体",
   "account.mention": "提及 @{name}",
-  "account.mute": "将 @{name} 静音",
+  "account.mute": "隐藏 @{name}",
+  "account.mute_notifications": "隐藏来自 @{name} 的通知",
+  "account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
   "account.posts": "嘟文",
   "account.report": "举报 @{name}",
-  "account.requested": "等待审批",
-  "account.share": "分享 @{name}的个人资料",
-  "account.unblock": "解除对 @{name} 的屏蔽",
-  "account.unblock_domain": "不再隐藏 {domain}",
+  "account.requested": "正在等待对方同意。点击以取消发送关注请求",
+  "account.share": "分享 @{name} 的个人资料",
+  "account.unblock": "不再屏蔽 @{name}",
+  "account.unblock_domain": "不再隐藏来自 {domain} 的内容",
   "account.unfollow": "取消关注",
-  "account.unmute": "取消 @{name} 的静音",
+  "account.unmute": "不再隐藏 @{name}",
   "account.view_full_profile": "查看完整资料",
-  "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
+  "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
   "bundle_column_error.body": "载入组件出错。",
   "bundle_column_error.retry": "重试",
   "bundle_column_error.title": "网络错误",
@@ -31,79 +33,80 @@
   "column.favourites": "收藏过的嘟文",
   "column.follow_requests": "关注请求",
   "column.home": "主页",
-  "column.mutes": "被静音的用户",
+  "column.mutes": "被隐藏的用户",
   "column.notifications": "通知",
   "column.pins": "置顶嘟文",
   "column.public": "跨站公共时间轴",
   "column_back_button.label": "返回",
   "column_header.hide_settings": "隐藏设置",
-  "column_header.moveLeft_settings": "将栏左移",
-  "column_header.moveRight_settings": "将栏右移",
+  "column_header.moveLeft_settings": "将此栏左移",
+  "column_header.moveRight_settings": "将此栏右移",
   "column_header.pin": "固定",
   "column_header.show_settings": "显示设置",
-  "column_header.unpin": "取下",
+  "column_header.unpin": "取消固定",
   "column_subheading.navigation": "导航",
   "column_subheading.settings": "设置",
-  "compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
+  "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以通过关注你来查看仅关注者可见的嘟文。",
   "compose_form.lock_disclaimer.lock": "被保护",
   "compose_form.placeholder": "在想啥?",
   "compose_form.publish": "嘟嘟",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "将媒体文件标示为“敏感内容”",
-  "compose_form.spoiler": "将部分文本藏于警告消息之后",
-  "compose_form.spoiler_placeholder": "敏感内容的警告消息",
+  "compose_form.sensitive": "将媒体文件标记为敏感内容",
+  "compose_form.spoiler": "折叠嘟文内容",
+  "compose_form.spoiler_placeholder": "折叠部分的警告消息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "屏蔽",
-  "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
+  "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
   "confirmations.delete.confirm": "删除",
-  "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
-  "confirmations.domain_block.confirm": "隐藏整个网站",
-  "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
-  "confirmations.mute.confirm": "静音",
-  "confirmations.mute.message": "想好了,真的要静音 {name}?",
+  "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
+  "confirmations.domain_block.confirm": "隐藏整个网站的内容",
+  "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。",
+  "confirmations.mute.confirm": "隐藏",
+  "confirmations.mute.message": "想好了,真的要隐藏 {name}?",
   "confirmations.unfollow.confirm": "取消关注",
-  "confirmations.unfollow.message": "确定要取消关注 {name}吗?",
-  "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
-  "embed.preview": "到时大概长这样:",
+  "confirmations.unfollow.message": "确定要取消关注 {name} 吗?",
+  "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。",
+  "embed.preview": "它会像这样显示出来:",
   "emoji_button.activity": "活动",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "自定义",
   "emoji_button.flags": "旗帜",
   "emoji_button.food": "食物和饮料",
   "emoji_button.label": "加入表情符号",
   "emoji_button.nature": "自然",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "木有这个表情符号!(╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物体",
   "emoji_button.people": "人物",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "常用",
   "emoji_button.search": "搜索…",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "搜索结果",
   "emoji_button.symbols": "符号",
-  "emoji_button.travel": "旅途和地点",
-  "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
-  "empty_column.hashtag": "这个标签暂时未有内容。",
+  "emoji_button.travel": "旅行和地点",
+  "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!",
+  "empty_column.hashtag": "这个话题标签下暂时没有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
   "empty_column.home.public_timeline": "公共时间轴",
-  "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
-  "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
-  "follow_request.authorize": "批准",
+  "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
+  "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
+  "follow_request.authorize": "同意",
   "follow_request.reject": "拒绝",
-  "getting_started.appsshort": "Apps",
-  "getting_started.faq": "FAQ",
+  "getting_started.appsshort": "应用",
+  "getting_started.faq": "常见问题",
   "getting_started.heading": "开始使用",
-  "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
+  "getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。",
   "getting_started.userguide": "用户指南",
-  "home.column_settings.advanced": "高端",
-  "home.column_settings.basic": "基本",
-  "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
-  "home.column_settings.show_reblogs": "显示被转的嘟文",
-  "home.column_settings.show_replies": "显示回应嘟文",
-  "home.settings": "字段设置",
+  "home.column_settings.advanced": "高级设置",
+  "home.column_settings.basic": "基本设置",
+  "home.column_settings.filter_regex": "使用正则表达式(regex)过滤",
+  "home.column_settings.show_reblogs": "显示转嘟",
+  "home.column_settings.show_replies": "显示回复",
+  "home.settings": "栏目设置",
   "lightbox.close": "关闭",
   "lightbox.next": "下一步",
   "lightbox.previous": "上一步",
   "loading_indicator.label": "加载中……",
-  "media_gallery.toggle_visible": "打开或关上",
+  "media_gallery.toggle_visible": "切换显示/隐藏",
   "missing_indicator.label": "找不到内容",
+  "mute_modal.hide_notifications": "隐藏来自这个用户的通知",
   "navigation_bar.blocks": "被屏蔽的用户",
   "navigation_bar.community_timeline": "本站时间轴",
   "navigation_bar.edit_profile": "修改个人资料",
@@ -111,7 +114,7 @@
   "navigation_bar.follow_requests": "关注请求",
   "navigation_bar.info": "关于本站",
   "navigation_bar.logout": "注销",
-  "navigation_bar.mutes": "被静音的用户",
+  "navigation_bar.mutes": "被隐藏的用户",
   "navigation_bar.pins": "置顶嘟文",
   "navigation_bar.preferences": "首选项",
   "navigation_bar.public_timeline": "跨站公共时间轴",
@@ -119,9 +122,9 @@
   "notification.follow": "{name} 开始关注你",
   "notification.mention": "{name} 提及你",
   "notification.reblog": "{name} 转嘟了你的嘟文",
-  "notifications.clear": "清空通知纪录",
-  "notifications.clear_confirmation": "你确定要清空通知纪录吗?",
-  "notifications.column_settings.alert": "显示桌面通知",
+  "notifications.clear": "清空通知列表",
+  "notifications.clear_confirmation": "你确定要清空通知列表吗?",
+  "notifications.column_settings.alert": "桌面通知",
   "notifications.column_settings.favourite": "你的嘟文被收藏:",
   "notifications.column_settings.follow": "关注你:",
   "notifications.column_settings.mention": "提及你:",
@@ -132,90 +135,91 @@
   "notifications.column_settings.sound": "播放音效",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
-  "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
-  "onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
-  "onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
-  "onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。",
-  "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。",
-  "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
+  "onboarding.page_five.public_timelines": "本站时间轴显示的是由本站({domain})用户发布的所有公开嘟文。跨站公共时间轴显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
+  "onboarding.page_four.home": "你的主页上的时间轴上显示的是你关注对象的嘟文。",
+  "onboarding.page_four.notifications": "如果有人与你互动,便会出现在通知栏中哦~",
+  "onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立但又相互连接的服务器叫做实例。",
+  "onboarding.page_one.handle": "你在 {domain},{handle} 就是你的完整帐户名称。",
+  "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
   "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
-  "onboarding.page_six.almost_done": "差不多了…",
-  "onboarding.page_six.appetoot": "嗷呜~",
-  "onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
-  "onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
+  "onboarding.page_six.almost_done": "差不多了……",
+  "onboarding.page_six.appetoot": "嗷呜~",
+  "onboarding.page_six.apps_available": "我们还有适用于 iOS、Android 和其它平台的{apps}哦~",
+  "onboarding.page_six.github": "Mastodon 是自由的开源软件。欢迎前往 {github} 反馈问题、提出对新功能的建议或贡献代码 :-)",
   "onboarding.page_six.guidelines": "社区指南",
-  "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!",
-  "onboarding.page_six.various_app": "移动应用程序",
-  "onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
-  "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)啦。",
-  "onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
-  "onboarding.skip": "好啦好啦我知道啦",
-  "privacy.change": "调整隐私设置",
-  "privacy.direct.long": "只有提及的用户能看到",
-  "privacy.direct.short": "私人消息",
-  "privacy.private.long": "只有关注你用户能看到",
-  "privacy.private.short": "关注者",
-  "privacy.public.long": "在公共时间轴显示",
-  "privacy.public.short": "公共",
-  "privacy.unlisted.long": "公开,但不在公共时间轴显示",
-  "privacy.unlisted.short": "公开",
-  "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",
+  "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}!",
+  "onboarding.page_six.various_app": "移动设备应用",
+  "onboarding.page_three.profile": "你可以修改你的个人资料,比如头像、简介和昵称等偏好设置。",
+  "onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整帐户名称(用户名@域名)哦。",
+  "onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别用来上传图片,修改嘟文可见范围,以及添加警告信息。",
+  "onboarding.skip": "跳过",
+  "privacy.change": "设置嘟文可见范围",
+  "privacy.direct.long": "只有被提及的用户能看到",
+  "privacy.direct.short": "私信",
+  "privacy.private.long": "只有关注你的用户能看到",
+  "privacy.private.short": "仅关注者",
+  "privacy.public.long": "所有人可见,并会出现在公共时间轴上",
+  "privacy.public.short": "公开",
+  "privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上",
+  "privacy.unlisted.short": "不公开",
+  "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.placeholder": "附言",
   "report.submit": "提交",
-  "report.target": "Reporting",
+  "report.target": "举报 {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_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "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": "大家都在干啥?",
-  "status.cannot_reblog": "没法转嘟这条嘟文啦……",
+  "status.cannot_reblog": "无法转嘟这条嘟文",
   "status.delete": "删除",
   "status.embed": "嵌入",
   "status.favourite": "收藏",
   "status.load_more": "加载更多",
   "status.media_hidden": "隐藏媒体内容",
   "status.mention": "提及 @{name}",
-  "status.more": "More",
-  "status.mute_conversation": "静音对话",
+  "status.more": "更多",
+  "status.mute_conversation": "隐藏此对话",
   "status.open": "展开嘟文",
-  "status.pin": "置顶到资料",
+  "status.pin": "在个人资料页面置顶",
   "status.reblog": "转嘟",
-  "status.reblogged_by": "{name} 转嘟",
-  "status.reply": "回应",
-  "status.replyAll": "回应整串",
+  "status.reblogged_by": "{name} 转嘟了",
+  "status.reply": "回复",
+  "status.replyAll": "回复所有人",
   "status.report": "举报 @{name}",
   "status.sensitive_toggle": "点击显示",
   "status.sensitive_warning": "敏感内容",
-  "status.share": "Share",
-  "status.show_less": "减少显示",
-  "status.show_more": "显示更多",
-  "status.unmute_conversation": "解禁对话",
-  "status.unpin": "解除置顶",
+  "status.share": "分享",
+  "status.show_less": "隐藏内容",
+  "status.show_more": "显示内容",
+  "status.unmute_conversation": "不再隐藏此对话",
+  "status.unpin": "在个人资料页面取消置顶",
   "tabs_bar.compose": "撰写",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主页",
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
-  "upload_area.title": "将文件拖放至此上传",
+  "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。",
+  "upload_area.title": "将文件拖放到此处开始上传",
   "upload_button.label": "上传媒体文件",
-  "upload_form.description": "Describe for the visually impaired",
-  "upload_form.undo": "还原",
-  "upload_progress.label": "上传中……",
-  "video.close": "关闭影片",
+  "upload_form.description": "为视觉障碍人士添加文字说明",
+  "upload_form.undo": "取消上传",
+  "upload_progress.label": "上传中…",
+  "video.close": "关闭视频",
   "video.exit_fullscreen": "退出全屏",
-  "video.expand": "展开影片",
+  "video.expand": "展开视频",
   "video.fullscreen": "全屏",
-  "video.hide": "隐藏影片",
+  "video.hide": "隐藏视频",
   "video.mute": "静音",
   "video.pause": "暂停",
   "video.play": "播放",
-  "video.unmute": "解除静音"
+  "video.unmute": "取消静音"
 }
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 251a40144..5d0acbd60 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -33,6 +33,7 @@ import { TIMELINE_DELETE } from '../actions/timelines';
 import { STORE_HYDRATE } from '../actions/store';
 import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
 import uuid from '../uuid';
+import { me } from '../initial_state';
 
 const initialState = ImmutableMap({
   mounted: false,
@@ -54,7 +55,6 @@ const initialState = ImmutableMap({
   media_attachments: ImmutableList(),
   suggestion_token: null,
   suggestions: ImmutableList(),
-  me: null,
   default_advanced_options: ImmutableMap({
     do_not_federate: false,
   }),
@@ -77,7 +77,6 @@ const initialState = ImmutableMap({
 
 function statusToTextMentions(state, status) {
   let set = ImmutableOrderedSet([]);
-  let me  = state.get('me');
 
   if (status.getIn(['account', 'id']) !== me) {
     set = set.add(`@${status.getIn(['account', 'acct'])} `);
diff --git a/app/javascript/mastodon/reducers/custom_emojis.js b/app/javascript/mastodon/reducers/custom_emojis.js
index f2a8ca5d2..307bcc7dc 100644
--- a/app/javascript/mastodon/reducers/custom_emojis.js
+++ b/app/javascript/mastodon/reducers/custom_emojis.js
@@ -8,7 +8,7 @@ const initialState = ImmutableList();
 export default function custom_emojis(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
-    emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', []), action.state.getIn(['meta', 'auto_play_gif'], false)) });
+    emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) });
     return action.state.get('custom_emojis');
   default:
     return state;
diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js
index 119ef9d8f..36a5a1c35 100644
--- a/app/javascript/mastodon/reducers/meta.js
+++ b/app/javascript/mastodon/reducers/meta.js
@@ -4,7 +4,6 @@ import { Map as ImmutableMap } from 'immutable';
 const initialState = ImmutableMap({
   streaming_api_base_url: null,
   access_token: null,
-  me: null,
 });
 
 export default function meta(state = initialState, action) {
diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js
index 496e6846a..a96232dbd 100644
--- a/app/javascript/mastodon/reducers/mutes.js
+++ b/app/javascript/mastodon/reducers/mutes.js
@@ -22,7 +22,7 @@ export default function mutes(state = initialState, action) {
       state.setIn(['new', 'notifications'], true);
     });
   case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
-    return state.setIn(['new', 'notifications'], !state.getIn(['new', 'notifications']));
+    return state.updateIn(['new', 'notifications'], (old) => !old);
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 4b36082b2..36c68ffc5 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -1,5 +1,66 @@
 import WebSocketClient from 'websocket.js';
 
+export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
+  return (dispatch, getState) => {
+    const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
+    const accessToken = getState().getIn(['meta', 'access_token']);
+    const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
+    let polling = null;
+
+    const setupPolling = () => {
+      polling = setInterval(() => {
+        pollingRefresh(dispatch);
+      }, 20000);
+    };
+
+    const clearPolling = () => {
+      if (polling) {
+        clearInterval(polling);
+        polling = null;
+      }
+    };
+
+    const subscription = getStream(streamingAPIBaseURL, accessToken, path, {
+      connected () {
+        if (pollingRefresh) {
+          clearPolling();
+        }
+        onConnect();
+      },
+
+      disconnected () {
+        if (pollingRefresh) {
+          setupPolling();
+        }
+        onDisconnect();
+      },
+
+      received (data) {
+        onReceive(data);
+      },
+
+      reconnected () {
+        if (pollingRefresh) {
+          clearPolling();
+          pollingRefresh(dispatch);
+        }
+        onConnect();
+      },
+
+    });
+
+    const disconnect = () => {
+      if (subscription) {
+        subscription.close();
+      }
+      clearPolling();
+    };
+
+    return disconnect;
+  };
+}
+
+
 export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
   const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
 
diff --git a/app/javascript/packs/custom.js b/app/javascript/packs/custom.js
deleted file mode 100644
index 4db2964f6..000000000
--- a/app/javascript/packs/custom.js
+++ /dev/null
@@ -1 +0,0 @@
-require('../styles/custom.scss');
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index b00dd8c1e..2cf98c642 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -571,7 +571,19 @@
   font-size: 12px;
   line-height: 12px;
   font-weight: 500;
-  color: $success-green;
-  background-color: rgba($success-green, 0.1);
-  border: 1px solid rgba($success-green, 0.5);
+  color: $ui-secondary-color;
+  background-color: rgba($ui-secondary-color, 0.1);
+  border: 1px solid rgba($ui-secondary-color, 0.5);
+
+  &.moderator {
+    color: $success-green;
+    background-color: rgba($success-green, 0.1);
+    border-color: rgba($success-green, 0.5);
+  }
+
+  &.admin {
+    color: lighten($error-red, 12%);
+    background-color: rgba(lighten($error-red, 12%), 0.1);
+    border-color: rgba(lighten($error-red, 12%), 0.5);
+  }
 }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2506bbe62..6a6d1bdca 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -253,6 +253,15 @@
   width: 0;
   height: 0;
   position: absolute;
+
+  img,
+  svg {
+    margin: 0 !important;
+    border: 0 !important;
+    padding: 0 !important;
+    width: 0 !important;
+    height: 0 !important;
+  }
 }
 
 .ellipsis {
@@ -555,6 +564,7 @@
   font-weight: 400;
   overflow: visible;
   white-space: pre-wrap;
+  padding-top: 5px;
 
   &.status__content--with-spoiler {
     white-space: normal;
@@ -565,8 +575,9 @@
   }
 
   .emojione {
-    width: 18px;
-    height: 18px;
+    width: 20px;
+    height: 20px;
+    margin: -5px 0 0;
   }
 
   p {
@@ -671,7 +682,7 @@
     outline: 0;
     background: lighten($ui-base-color, 4%);
 
-    &.status-direct {
+    .status.status-direct {
       background: lighten($ui-base-color, 12%);
     }
 
@@ -690,6 +701,12 @@
   border-bottom: 1px solid lighten($ui-base-color, 8%);
   cursor: default;
 
+  @supports (-ms-overflow-style: -ms-autohiding-scrollbar) {
+    // Add margin to avoid Edge auto-hiding scrollbar appearing over content.
+    // On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px.
+    padding-right: 26px; // 10px + 16px
+  }
+
   @keyframes fade {
     0% { opacity: 0; }
     100% { opacity: 1; }
@@ -920,8 +937,9 @@
     line-height: 24px;
 
     .emojione {
-      width: 22px;
-      height: 22px;
+      width: 24px;
+      height: 24px;
+      margin: -5px 0 0;
     }
   }
 
@@ -2908,7 +2926,7 @@ button.icon-button.active i.fa-retweet {
   color: $primary-text-color;
   position: absolute;
   top: 10px;
-  right: 10px;
+  left: 10px;
   opacity: 0.7;
   display: inline-block;
   vertical-align: top;
@@ -2923,7 +2941,7 @@ button.icon-button.active i.fa-retweet {
 .account--action-button {
   position: absolute;
   top: 10px;
-  left: 20px;
+  right: 20px;
 }
 
 .setting-toggle {
@@ -3973,6 +3991,14 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.mute-modal {
+  line-height: 24px;
+}
+
+.mute-modal .react-toggle {
+  vertical-align: middle;
+}
+
 .report-modal__statuses,
 .report-modal__comment {
   padding: 10px;
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 15ff84912..0bf9daafd 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -1,4 +1,5 @@
-.landing-strip {
+.landing-strip,
+.memoriam-strip {
   background: rgba(darken($ui-base-color, 7%), 0.8);
   color: $ui-primary-color;
   font-weight: 400;
@@ -29,3 +30,7 @@
     margin-bottom: 0;
   }
 }
+
+.memoriam-strip {
+  background: rgba($base-shadow-color, 0.7);
+}
diff --git a/app/javascript/themes/default/theme.yml b/app/javascript/themes/default/theme.yml
index 6a7a872b4..0b262cc82 100644
--- a/app/javascript/themes/default/theme.yml
+++ b/app/javascript/themes/default/theme.yml
@@ -1,9 +1,18 @@
-#  (REQUIRED) Name must be unique across all installed themes.
-name: default
-
 #  (REQUIRED) The location of the pack file inside `pack_directory`.
 pack: application.js
 
 #  (OPTIONAL) The directory which contains the pack file.
-#  Defaults to the theme directory (`app/javascript/themes/[theme]`).
+#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
+#  but in the case of the vanilla Mastodon theme the pack file is
+#  somewhere else.
 pack_directory: app/javascript/packs
+
+#  (OPTIONAL) Additional javascript resources to preload, for use with
+#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
+#  derive these pathnames from `themes/[your-theme]` to ensure that
+#  they stay unique. (Of course, vanilla doesn't do this ^^;;)
+preload:
+- features/getting_started
+- features/compose
+- features/home_timeline
+- features/notifications
diff --git a/app/javascript/themes/spin/pack.js b/app/javascript/themes/spin/pack.js
deleted file mode 100644
index b11ac4802..000000000
--- a/app/javascript/themes/spin/pack.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import '../../packs/application';
-import './style.scss';
diff --git a/app/javascript/themes/spin/style.scss b/app/javascript/themes/spin/style.scss
deleted file mode 100644
index 1a9381fd0..000000000
--- a/app/javascript/themes/spin/style.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-:root:root:root {
-  .button, .icon-button, .emoji-button, .account__avatar, .account__avatar-overlay {
-    animation: spin 4s linear infinite;
-  }
-}
-
-@keyframes spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
diff --git a/app/javascript/themes/spin/theme.yml b/app/javascript/themes/spin/theme.yml
deleted file mode 100644
index a684997dc..000000000
--- a/app/javascript/themes/spin/theme.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-name: spin
-pack: pack.js
\ No newline at end of file