about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/components/actions/accounts.jsx9
-rw-r--r--app/assets/javascripts/components/actions/meta.jsx8
-rw-r--r--app/assets/javascripts/components/actions/notifications.jsx10
-rw-r--r--app/assets/javascripts/components/actions/settings.jsx17
-rw-r--r--app/assets/javascripts/components/actions/store.jsx17
-rw-r--r--app/assets/javascripts/components/components/status_action_bar.jsx2
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx28
-rw-r--r--app/assets/javascripts/components/containers/status_container.jsx6
-rw-r--r--app/assets/javascripts/components/features/account/index.jsx8
-rw-r--r--app/assets/javascripts/components/features/compose/containers/navigation_container.jsx8
-rw-r--r--app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx6
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx4
-rw-r--r--app/assets/javascripts/components/features/ui/index.jsx5
-rw-r--r--app/assets/javascripts/components/is_mobile.jsx5
-rw-r--r--app/assets/javascripts/components/locales/de.jsx18
-rw-r--r--app/assets/javascripts/components/locales/en.jsx2
-rw-r--r--app/assets/javascripts/components/reducers/accounts.jsx69
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx8
-rw-r--r--app/assets/javascripts/components/reducers/index.jsx4
-rw-r--r--app/assets/javascripts/components/reducers/meta.jsx18
-rw-r--r--app/assets/javascripts/components/reducers/notifications.jsx41
-rw-r--r--app/assets/javascripts/components/reducers/settings.jsx32
-rw-r--r--app/assets/javascripts/components/store/configureStore.jsx13
-rw-r--r--app/controllers/api/web/settings_controller.rb15
-rw-r--r--app/controllers/home_controller.rb1
-rw-r--r--app/helpers/home_helper.rb2
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/web.rb5
-rw-r--r--app/models/web/setting.rb7
-rw-r--r--app/views/home/index.html.haml3
-rw-r--r--app/views/home/initial_state.json.rabl24
32 files changed, 257 insertions, 144 deletions
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
index 8d28b051f..7ae87f30e 100644
--- a/app/assets/javascripts/components/actions/accounts.jsx
+++ b/app/assets/javascripts/components/actions/accounts.jsx
@@ -1,8 +1,6 @@
 import api, { getLinks } from '../api'
 import Immutable from 'immutable';
 
-export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF';
-
 export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
 export const ACCOUNT_FETCH_FAIL    = 'ACCOUNT_FETCH_FAIL';
@@ -67,13 +65,6 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
 export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
 export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 
-export function setAccountSelf(account) {
-  return {
-    type: ACCOUNT_SET_SELF,
-    account
-  };
-};
-
 export function fetchAccount(id) {
   return (dispatch, getState) => {
     dispatch(fetchAccountRequest(id));
diff --git a/app/assets/javascripts/components/actions/meta.jsx b/app/assets/javascripts/components/actions/meta.jsx
deleted file mode 100644
index d0adbce3f..000000000
--- a/app/assets/javascripts/components/actions/meta.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-export const ACCESS_TOKEN_SET = 'ACCESS_TOKEN_SET';
-
-export function setAccessToken(token) {
-  return {
-    type: ACCESS_TOKEN_SET,
-    token: token
-  };
-};
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx
index 182b598aa..1e5b2c382 100644
--- a/app/assets/javascripts/components/actions/notifications.jsx
+++ b/app/assets/javascripts/components/actions/notifications.jsx
@@ -14,8 +14,6 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
 export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
 export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
 
-export const NOTIFICATIONS_SETTING_CHANGE = 'NOTIFICATIONS_SETTING_CHANGE';
-
 const fetchRelatedRelationships = (dispatch, notifications) => {
   const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
 
@@ -133,11 +131,3 @@ export function expandNotificationsFail(error) {
     error
   };
 };
-
-export function changeNotificationsSetting(key, checked) {
-  return {
-    type: NOTIFICATIONS_SETTING_CHANGE,
-    key,
-    checked
-  };
-};
diff --git a/app/assets/javascripts/components/actions/settings.jsx b/app/assets/javascripts/components/actions/settings.jsx
new file mode 100644
index 000000000..0a6fb7cdb
--- /dev/null
+++ b/app/assets/javascripts/components/actions/settings.jsx
@@ -0,0 +1,17 @@
+import axios from 'axios';
+
+export const SETTING_CHANGE = 'SETTING_CHANGE';
+
+export function changeSetting(key, value) {
+  return (dispatch, getState) => {
+    dispatch({
+      type: SETTING_CHANGE,
+      key,
+      value
+    });
+
+    axios.put('/api/web/settings', {
+      data: getState().get('settings').toJS()
+    });
+  };
+};
diff --git a/app/assets/javascripts/components/actions/store.jsx b/app/assets/javascripts/components/actions/store.jsx
new file mode 100644
index 000000000..3bba99549
--- /dev/null
+++ b/app/assets/javascripts/components/actions/store.jsx
@@ -0,0 +1,17 @@
+import Immutable from 'immutable';
+
+export const STORE_HYDRATE = 'STORE_HYDRATE';
+
+const convertState = rawState =>
+  Immutable.fromJS(rawState, (k, v) =>
+    Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
+      Number.isNaN(x * 1) ? x : x * 1));
+
+export function hydrateStore(rawState) {
+  const state = convertState(rawState);
+
+  return {
+    type: STORE_HYDRATE,
+    state
+  };
+};
diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx
index 26f9d3c27..6ada59c0d 100644
--- a/app/assets/javascripts/components/components/status_action_bar.jsx
+++ b/app/assets/javascripts/components/components/status_action_bar.jsx
@@ -49,7 +49,7 @@ const StatusActionBar = React.createClass({
   },
 
   handleMentionClick () {
-    this.props.onMention(this.props.status.get('account'));
+    this.props.onMention(this.props.status.get('account'), this.context.router);
   },
 
   handleBlockClick () {
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 6c0d28053..143a280c3 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -7,8 +7,6 @@ import {
   refreshTimeline
 } from '../actions/timelines';
 import { updateNotifications } from '../actions/notifications';
-import { setAccessToken } from '../actions/meta';
-import { setAccountSelf } from '../actions/accounts';
 import createBrowserHistory from 'history/lib/createBrowserHistory';
 import {
   applyRouterMiddleware,
@@ -44,9 +42,12 @@ import pt from 'react-intl/locale-data/pt';
 import hu from 'react-intl/locale-data/hu';
 import uk from 'react-intl/locale-data/uk';
 import getMessagesForLocale from '../locales';
+import { hydrateStore } from '../actions/store';
 
 const store = configureStore();
 
+store.dispatch(hydrateStore(window.INITIAL_STATE));
+
 const browserHistory = useRouterHistory(createBrowserHistory)({
   basename: '/web'
 });
@@ -56,29 +57,26 @@ addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]);
 const Mastodon = React.createClass({
 
   propTypes: {
-    token: React.PropTypes.string.isRequired,
-    timelines: React.PropTypes.object,
-    account: React.PropTypes.string,
     locale: React.PropTypes.string.isRequired
   },
 
   componentWillMount() {
-    const { token, account, locale } = this.props;
-
-    store.dispatch(setAccessToken(token));
-    store.dispatch(setAccountSelf(JSON.parse(account)));
+    const { locale } = this.props;
 
     if (typeof App !== 'undefined') {
       this.subscription = App.cable.subscriptions.create('TimelineChannel', {
 
         received (data) {
           switch(data.type) {
-            case 'update':
-              return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message)));
-            case 'delete':
-              return store.dispatch(deleteFromTimelines(data.id));
-            case 'notification':
-              return store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale));
+          case 'update':
+            store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message)));
+            break;
+          case 'delete':
+            store.dispatch(deleteFromTimelines(data.id));
+            break;
+          case 'notification':
+            store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale));
+            break;
           }
         }
 
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index 6a882eab4..ad2be03d1 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -15,6 +15,7 @@ import { blockAccount } from '../actions/accounts';
 import { deleteStatus } from '../actions/statuses';
 import { openMedia } from '../actions/modal';
 import { createSelector } from 'reselect'
+import { isMobile } from '../is_mobile'
 
 const mapStateToProps = (state, props) => ({
   statusBase: state.getIn(['statuses', props.id]),
@@ -86,8 +87,11 @@ const mapDispatchToProps = (dispatch) => ({
     dispatch(deleteStatus(status.get('id')));
   },
 
-  onMention (account) {
+  onMention (account, router) {
     dispatch(mentionCompose(account));
+    if (isMobile(window.innerWidth)) {
+      router.push('/statuses/new');
+    }
   },
 
   onOpenMedia (url) {
diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx
index c2cc58bb2..2a9eba28a 100644
--- a/app/assets/javascripts/components/features/account/index.jsx
+++ b/app/assets/javascripts/components/features/account/index.jsx
@@ -20,6 +20,7 @@ import LoadingIndicator      from '../../components/loading_indicator';
 import ActionBar             from './components/action_bar';
 import Column                from '../ui/components/column';
 import ColumnBackButton      from '../../components/column_back_button';
+import { isMobile } from '../../is_mobile'
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
@@ -34,6 +35,10 @@ const makeMapStateToProps = () => {
 
 const Account = React.createClass({
 
+  contextTypes: {
+    router: React.PropTypes.object
+  },
+
   propTypes: {
     params: React.PropTypes.object.isRequired,
     dispatch: React.PropTypes.func.isRequired,
@@ -71,6 +76,9 @@ const Account = React.createClass({
 
   handleMention () {
     this.props.dispatch(mentionCompose(this.props.account));
+    if (isMobile(window.innerWidth)) {
+      this.context.router.push('/statuses/new');
+    }
   },
 
   render () {
diff --git a/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx b/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx
index 51e2513d8..0006608da 100644
--- a/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx
@@ -1,8 +1,10 @@
 import { connect }   from 'react-redux';
 import NavigationBar from '../components/navigation_bar';
 
-const mapStateToProps = (state, props) => ({
-  account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
-});
+const mapStateToProps = (state, props) => {
+  return {
+    account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
+  };
+};
 
 export default connect(mapStateToProps)(NavigationBar);
diff --git a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
index 6907fd351..5792e97e3 100644
--- a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
+++ b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
@@ -1,15 +1,15 @@
 import { connect } from 'react-redux';
 import ColumnSettings from '../components/column_settings';
-import { changeNotificationsSetting } from '../../../actions/notifications';
+import { changeSetting } from '../../../actions/settings';
 
 const mapStateToProps = state => ({
-  settings: state.getIn(['notifications', 'settings'])
+  settings: state.getIn(['settings', 'notifications'])
 });
 
 const mapDispatchToProps = dispatch => ({
 
   onChange (key, checked) {
-    dispatch(changeNotificationsSetting(key, checked));
+    dispatch(changeSetting(['notifications', ...key], checked));
   }
 
 });
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index 7e706ad6a..29be491eb 100644
--- a/app/assets/javascripts/components/features/notifications/index.jsx
+++ b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -18,7 +18,7 @@ const messages = defineMessages({
 });
 
 const getNotifications = createSelector([
-  state => Immutable.List(state.getIn(['notifications', 'settings', 'shows']).filter(item => !item).keys()),
+  state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
   state => state.getIn(['notifications', 'items'])
 ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
 
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 0a1528fe9..27a252759 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -23,6 +23,7 @@ import { ScrollContainer }   from 'react-router-scroll';
 import ColumnBackButton      from '../../components/column_back_button';
 import StatusContainer       from '../../containers/status_container';
 import { openMedia }         from '../../actions/modal';
+import { isMobile } from '../../is_mobile'
 
 const makeMapStateToProps = () => {
   const getStatus = makeGetStatus();
@@ -80,6 +81,9 @@ const Status = React.createClass({
 
   handleMentionClick (account) {
     this.props.dispatch(mentionCompose(account));
+    if (isMobile(window.innerWidth)) {
+      this.context.router.push('/statuses/new');
+    }
   },
 
   handleOpenMedia (url) {
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
index db793f945..ee2e29d6f 100644
--- a/app/assets/javascripts/components/features/ui/index.jsx
+++ b/app/assets/javascripts/components/features/ui/index.jsx
@@ -11,6 +11,7 @@ import Notifications from '../notifications';
 import { debounce } from 'react-decoration';
 import { uploadCompose } from '../../actions/compose';
 import { connect } from 'react-redux';
+import { isMobile } from '../../is_mobile'
 
 const UI = React.createClass({
 
@@ -64,11 +65,9 @@ const UI = React.createClass({
   },
 
   render () {
-    const layoutBreakpoint = 1024;
-
     let mountedColumns;
 
-    if (this.state.width <= layoutBreakpoint) {
+    if (isMobile(this.state.width)) {
       mountedColumns = (
         <ColumnsArea>
           {this.props.children}
diff --git a/app/assets/javascripts/components/is_mobile.jsx b/app/assets/javascripts/components/is_mobile.jsx
new file mode 100644
index 000000000..eaa6221e4
--- /dev/null
+++ b/app/assets/javascripts/components/is_mobile.jsx
@@ -0,0 +1,5 @@
+const LAYOUT_BREAKPOINT = 1024;
+
+export function isMobile(width) {
+  return width <= LAYOUT_BREAKPOINT;
+};
diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx
index 97df67480..c37a71c21 100644
--- a/app/assets/javascripts/components/locales/de.jsx
+++ b/app/assets/javascripts/components/locales/de.jsx
@@ -8,6 +8,9 @@ const en = {
   "status.reblog": "Teilen",
   "status.favourite": "Favorisieren",
   "status.reblogged_by": "{name} teilte",
+  "status.sensitive_warning": "Sensible Inhalte",
+  "status.sensitive_toggle": "Klicken um zu zeigen",
+  "status.open": "Öffnen",
   "video_player.toggle_sound": "Ton umschalten",
   "account.mention": "Erwähnen",
   "account.edit_profile": "Profil bearbeiten",
@@ -19,14 +22,17 @@ const en = {
   "account.follows": "Folgt",
   "account.followers": "Folger",
   "account.follows_you": "Folgt dir",
+  "account.requested": "Warte auf Erlaubnis",
   "getting_started.heading": "Erste Schritte",
   "getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
   "getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
   "getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
+  "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
   "column.home": "Home",
   "column.mentions": "Erwähnungen",
   "column.public": "Gesamtes Bekanntes Netz",
   "column.notifications": "Mitteilungen",
+  "column.follow_requests": "Folgeanfragen",
   "tabs_bar.compose": "Schreiben",
   "tabs_bar.home": "Home",
   "tabs_bar.mentions": "Erwähnungen",
@@ -36,10 +42,12 @@ const en = {
   "compose_form.publish": "Veröffentlichen",
   "compose_form.sensitive": "Medien als sensitiv markieren",
   "compose_form.unlisted": "Öffentlich nicht auflisten",
+  "compose_form.private": "Als privat markieren",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.preferences": "Einstellungen",
   "navigation_bar.public_timeline": "Öffentlich",
   "navigation_bar.logout": "Abmelden",
+  "navigation_bar.follow_requests": "Folgeanfragen",
   "reply_indicator.cancel": "Abbrechen",
   "search.placeholder": "Suche",
   "search.account": "Konto",
@@ -49,7 +57,15 @@ const en = {
   "notification.follow": "{name} folgt dir",
   "notification.favourite": "{name} favorisierte deinen Status",
   "notification.reblog": "{name} teilte deinen Status",
-  "notification.mention": "{name} erwähnte dich"
+  "notification.mention": "{name} erwähnte dich",
+  "notifications.column_settings.alert": "Desktop-Benachrichtigunen",
+  "notifications.column_settings.show": "In der Spalte anzeigen",
+  "notifications.column_settings.follow": "Neue Folger:",
+  "notifications.column_settings.favourite": "Favorisierungen:",
+  "notifications.column_settings.mention": "Erwähnungen:",
+  "notifications.column_settings.reblog": "Geteilte Beiträge:",
+  "follow_request.authorize": "Erlauben",
+  "follow_request.reject": "Ablehnen"
 };
 
 export default en;
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index b166c48ba..92dcbaeb9 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -17,7 +17,6 @@ const en = {
   "account.unfollow": "Unfollow",
   "account.block": "Block",
   "account.follow": "Follow",
-  "account.block": "Block",
   "account.posts": "Posts",
   "account.follows": "Follows",
   "account.followers": "Followers",
@@ -41,6 +40,7 @@ const en = {
   "compose_form.publish": "Toot",
   "compose_form.sensitive": "Mark media as sensitive",
   "compose_form.private": "Mark as private",
+  "compose_form.unlisted": "Do not display in public timeline",
   "navigation_bar.edit_profile": "Edit profile",
   "navigation_bar.preferences": "Preferences",
   "navigation_bar.public_timeline": "Public timeline",
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index 7f2f89d0a..ae048df3b 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -1,5 +1,4 @@
 import {
-  ACCOUNT_SET_SELF,
   ACCOUNT_FETCH_SUCCESS,
   FOLLOWERS_FETCH_SUCCESS,
   FOLLOWERS_EXPAND_SUCCESS,
@@ -33,6 +32,7 @@ import {
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS
 } from '../actions/notifications';
+import { STORE_HYDRATE } from '../actions/store';
 import Immutable from 'immutable';
 
 const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account));
@@ -67,38 +67,39 @@ const initialState = Immutable.Map();
 
 export default function accounts(state = initialState, action) {
   switch(action.type) {
-    case ACCOUNT_SET_SELF:
-    case ACCOUNT_FETCH_SUCCESS:
-    case NOTIFICATIONS_UPDATE:
-      return normalizeAccount(state, action.account);
-    case FOLLOWERS_FETCH_SUCCESS:
-    case FOLLOWERS_EXPAND_SUCCESS:
-    case FOLLOWING_FETCH_SUCCESS:
-    case FOLLOWING_EXPAND_SUCCESS:
-    case REBLOGS_FETCH_SUCCESS:
-    case FAVOURITES_FETCH_SUCCESS:
-    case COMPOSE_SUGGESTIONS_READY:
-    case SEARCH_SUGGESTIONS_READY:
-    case FOLLOW_REQUESTS_FETCH_SUCCESS:
-      return normalizeAccounts(state, action.accounts);
-    case NOTIFICATIONS_REFRESH_SUCCESS:
-    case NOTIFICATIONS_EXPAND_SUCCESS:
-      return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
-    case TIMELINE_REFRESH_SUCCESS:
-    case TIMELINE_EXPAND_SUCCESS:
-    case ACCOUNT_TIMELINE_FETCH_SUCCESS:
-    case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
-    case CONTEXT_FETCH_SUCCESS:
-      return normalizeAccountsFromStatuses(state, action.statuses);
-    case REBLOG_SUCCESS:
-    case FAVOURITE_SUCCESS:
-    case UNREBLOG_SUCCESS:
-    case UNFAVOURITE_SUCCESS:
-      return normalizeAccountFromStatus(state, action.response);
-    case TIMELINE_UPDATE:
-    case STATUS_FETCH_SUCCESS:
-      return normalizeAccountFromStatus(state, action.status);
-    default:
-      return state;
+  case STORE_HYDRATE:
+    return state.merge(action.state.get('accounts'));
+  case ACCOUNT_FETCH_SUCCESS:
+  case NOTIFICATIONS_UPDATE:
+    return normalizeAccount(state, action.account);
+  case FOLLOWERS_FETCH_SUCCESS:
+  case FOLLOWERS_EXPAND_SUCCESS:
+  case FOLLOWING_FETCH_SUCCESS:
+  case FOLLOWING_EXPAND_SUCCESS:
+  case REBLOGS_FETCH_SUCCESS:
+  case FAVOURITES_FETCH_SUCCESS:
+  case COMPOSE_SUGGESTIONS_READY:
+  case SEARCH_SUGGESTIONS_READY:
+  case FOLLOW_REQUESTS_FETCH_SUCCESS:
+    return normalizeAccounts(state, action.accounts);
+  case NOTIFICATIONS_REFRESH_SUCCESS:
+  case NOTIFICATIONS_EXPAND_SUCCESS:
+    return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
+  case TIMELINE_REFRESH_SUCCESS:
+  case TIMELINE_EXPAND_SUCCESS:
+  case ACCOUNT_TIMELINE_FETCH_SUCCESS:
+  case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
+  case CONTEXT_FETCH_SUCCESS:
+    return normalizeAccountsFromStatuses(state, action.statuses);
+  case REBLOG_SUCCESS:
+  case FAVOURITE_SUCCESS:
+  case UNREBLOG_SUCCESS:
+  case UNFAVOURITE_SUCCESS:
+    return normalizeAccountFromStatus(state, action.response);
+  case TIMELINE_UPDATE:
+  case STATUS_FETCH_SUCCESS:
+    return normalizeAccountFromStatus(state, action.status);
+  default:
+    return state;
   }
 };
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index 16215684e..baa7d7f5a 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -21,7 +21,7 @@ import {
   COMPOSE_LISTABILITY_CHANGE
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
-import { ACCOUNT_SET_SELF } from '../actions/accounts';
+import { STORE_HYDRATE } from '../actions/store';
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
@@ -88,6 +88,8 @@ const insertSuggestion = (state, position, token, completion) => {
 
 export default function compose(state = initialState, action) {
   switch(action.type) {
+    case STORE_HYDRATE:
+      return state.merge(action.state.get('compose'));
     case COMPOSE_MOUNT:
       return state.set('mounted', true);
     case COMPOSE_UNMOUNT:
@@ -97,7 +99,7 @@ export default function compose(state = initialState, action) {
     case COMPOSE_VISIBILITY_CHANGE:
       return state.set('private', action.checked);
     case COMPOSE_LISTABILITY_CHANGE:
-      return state.set('unlisted', action.checked);      
+      return state.set('unlisted', action.checked);
     case COMPOSE_CHANGE:
       return state.set('text', action.text);
     case COMPOSE_REPLY:
@@ -143,8 +145,6 @@ export default function compose(state = initialState, action) {
       } else {
         return state;
       }
-    case ACCOUNT_SET_SELF:
-      return state.set('me', action.account.id).set('private', action.account.locked);
     default:
       return state;
   }
diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx
index aea9239f8..068491949 100644
--- a/app/assets/javascripts/components/reducers/index.jsx
+++ b/app/assets/javascripts/components/reducers/index.jsx
@@ -11,6 +11,7 @@ import statuses from './statuses';
 import relationships from './relationships';
 import search from './search';
 import notifications from './notifications';
+import settings from './settings';
 
 export default combineReducers({
   timelines,
@@ -24,5 +25,6 @@ export default combineReducers({
   statuses,
   relationships,
   search,
-  notifications
+  notifications,
+  settings
 });
diff --git a/app/assets/javascripts/components/reducers/meta.jsx b/app/assets/javascripts/components/reducers/meta.jsx
index c7222c60b..cd4b313d5 100644
--- a/app/assets/javascripts/components/reducers/meta.jsx
+++ b/app/assets/javascripts/components/reducers/meta.jsx
@@ -1,16 +1,16 @@
-import { ACCESS_TOKEN_SET } from '../actions/meta';
-import { ACCOUNT_SET_SELF } from '../actions/accounts';
+import { STORE_HYDRATE } from '../actions/store';
 import Immutable from 'immutable';
 
-const initialState = Immutable.Map();
+const initialState = Immutable.Map({
+  access_token: null,
+  me: null
+});
 
 export default function meta(state = initialState, action) {
   switch(action.type) {
-    case ACCESS_TOKEN_SET:
-      return state.set('access_token', action.token);
-    case ACCOUNT_SET_SELF:
-      return state.set('me', action.account.id);
-    default:
-      return state;
+  case STORE_HYDRATE:
+    return state.merge(action.state.get('meta'));
+  default:
+    return state;
   }
 };
diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx
index e0d1ccf83..c85e7b460 100644
--- a/app/assets/javascripts/components/reducers/notifications.jsx
+++ b/app/assets/javascripts/components/reducers/notifications.jsx
@@ -2,7 +2,6 @@ import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-  NOTIFICATIONS_SETTING_CHANGE
 } from '../actions/notifications';
 import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 import Immutable from 'immutable';
@@ -10,23 +9,7 @@ import Immutable from 'immutable';
 const initialState = Immutable.Map({
   items: Immutable.List(),
   next: null,
-  loaded: false,
-
-  settings: Immutable.Map({
-    alerts: Immutable.Map({
-      follow: true,
-      favourite: true,
-      reblog: true,
-      mention: true
-    }),
-
-    shows: Immutable.Map({
-      follow: true,
-      favourite: true,
-      reblog: true,
-      mention: true
-    })
-  })
+  loaded: false
 });
 
 const notificationToMap = notification => Immutable.Map({
@@ -67,17 +50,15 @@ const filterNotifications = (state, relationship) => {
 
 export default function notifications(state = initialState, action) {
   switch(action.type) {
-    case NOTIFICATIONS_UPDATE:
-      return normalizeNotification(state, action.notification);
-    case NOTIFICATIONS_REFRESH_SUCCESS:
-      return normalizeNotifications(state, action.notifications, action.next);
-    case NOTIFICATIONS_EXPAND_SUCCESS:
-      return appendNormalizedNotifications(state, action.notifications, action.next);
-    case ACCOUNT_BLOCK_SUCCESS:
-      return filterNotifications(state, action.relationship);
-    case NOTIFICATIONS_SETTING_CHANGE:
-      return state.setIn(['settings', ...action.key], action.checked);
-    default:
-      return state;
+  case NOTIFICATIONS_UPDATE:
+    return normalizeNotification(state, action.notification);
+  case NOTIFICATIONS_REFRESH_SUCCESS:
+    return normalizeNotifications(state, action.notifications, action.next);
+  case NOTIFICATIONS_EXPAND_SUCCESS:
+    return appendNormalizedNotifications(state, action.notifications, action.next);
+  case ACCOUNT_BLOCK_SUCCESS:
+    return filterNotifications(state, action.relationship);
+  default:
+    return state;
   }
 };
diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx
new file mode 100644
index 000000000..2a834d81c
--- /dev/null
+++ b/app/assets/javascripts/components/reducers/settings.jsx
@@ -0,0 +1,32 @@
+import { SETTING_CHANGE } from '../actions/settings';
+import { STORE_HYDRATE } from '../actions/store';
+import Immutable from 'immutable';
+
+const initialState = Immutable.Map({
+  notifications: Immutable.Map({
+    alerts: Immutable.Map({
+      follow: true,
+      favourite: true,
+      reblog: true,
+      mention: true
+    }),
+
+    shows: Immutable.Map({
+      follow: true,
+      favourite: true,
+      reblog: true,
+      mention: true
+    })
+  })
+});
+
+export default function settings(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return state.merge(action.state.get('settings'));
+  case SETTING_CHANGE:
+    return state.setIn(action.key, action.value);
+  default:
+    return state;
+  }
+};
diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx
index 3d03d4c19..2c1476e5d 100644
--- a/app/assets/javascripts/components/store/configureStore.jsx
+++ b/app/assets/javascripts/components/store/configureStore.jsx
@@ -1,11 +1,12 @@
 import { createStore, applyMiddleware, compose } from 'redux';
-import thunk                                     from 'redux-thunk';
-import appReducer                                from '../reducers';
-import { loadingBarMiddleware }                  from 'react-redux-loading-bar';
-import errorsMiddleware                          from '../middleware/errors';
+import thunk from 'redux-thunk';
+import appReducer from '../reducers';
+import { loadingBarMiddleware } from 'react-redux-loading-bar';
+import errorsMiddleware from '../middleware/errors';
+import Immutable from 'immutable';
 
-export default function configureStore(initialState) {
-  return createStore(appReducer, initialState, compose(applyMiddleware(thunk, loadingBarMiddleware({
+export default function configureStore() {
+  return createStore(appReducer, compose(applyMiddleware(thunk, loadingBarMiddleware({
     promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
   }), errorsMiddleware()), window.devToolsExtension ? window.devToolsExtension() : f => f));
 };
diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb
new file mode 100644
index 000000000..e6f690114
--- /dev/null
+++ b/app/controllers/api/web/settings_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Api::Web::SettingsController < ApiController
+  respond_to :json
+
+  before_action :require_user!
+
+  def update
+    setting      = Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
+    setting.data = params[:data]
+    setting.save!
+
+    render_empty
+  end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index a25fe77da..814b1f758 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -6,6 +6,7 @@ class HomeController < ApplicationController
   def index
     @body_classes = 'app-body'
     @token        = find_or_create_access_token.token
+    @web_settings = Web::Setting.find_by(user: current_user)&.data || {}
   end
 
   private
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index 6f87c7b72..d3c6b13a6 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -3,8 +3,6 @@
 module HomeHelper
   def default_props
     {
-      token: @token,
-      account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json),
       locale: I18n.locale,
     }
   end
diff --git a/app/models/account.rb b/app/models/account.rb
index ba24cf153..ec0e81f7c 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -104,7 +104,7 @@ class Account < ApplicationRecord
   end
 
   def subscribed?
-    subscription_expires_at
+    !subscription_expires_at.blank?
   end
 
   def favourited?(status)
@@ -189,7 +189,7 @@ class Account < ApplicationRecord
     def requested_map(target_account_ids, account_id)
       follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
     end
-    
+
     private
 
     def follow_mapping(query, field)
diff --git a/app/models/web.rb b/app/models/web.rb
new file mode 100644
index 000000000..3c6eebbe2
--- /dev/null
+++ b/app/models/web.rb
@@ -0,0 +1,5 @@
+module Web
+  def self.table_name_prefix
+    'web_'
+  end
+end
diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb
new file mode 100644
index 000000000..3d601189b
--- /dev/null
+++ b/app/models/web/setting.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Web::Setting < ApplicationRecord
+  belongs_to :user
+
+  validates :user, uniqueness: true
+end
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 498fae105..730249129 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,4 +1,7 @@
 - content_for :header_tags do
+  :javascript
+    window.INITIAL_STATE = #{render(file: 'home/initial_state', formats: :json)}
+
   = javascript_include_tag 'application'
 
 = react_component 'Mastodon', default_props, class: 'app-holder', prerender: false
diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
new file mode 100644
index 000000000..0e9736f5f
--- /dev/null
+++ b/app/views/home/initial_state.json.rabl
@@ -0,0 +1,24 @@
+object false
+
+node(:meta) {
+  {
+    access_token: @token,
+    locale: I18n.locale,
+    me: current_account.id,
+  }
+}
+
+node(:compose) {
+  {
+    me: current_account.id,
+    private: current_account.locked?,
+  }
+}
+
+node(:accounts) {
+  {
+    current_account.id => partial('api/v1/accounts/show', object: current_account),
+  }
+}
+
+node(:settings) { @web_settings }