Repository: busyorg/busy
Pull request #730 (2017-09-12 12:48:30)

Use SteemConnect metadata for storing user settings and data
Syncing state to local storage with `redux-localstorage` is problematic and causes problems when updating state architecture. Furthermore, local storage is device specific so draft posts and other settings won't be accessible from different devices.

Migrated to metadata
- [x] Language
- [x] Draft posts
- [x] Bookmarks

https://trello.com/c/dbDNU0YZ/254-migrate-from-localstorage-to-sc2-metadata


Patch:
diff --git a/package-lock.json b/package-lock.json
index e666ff5d..3c259b5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13517,11 +13517,6 @@
       "resolved": "https://registry.npmjs.org/redux-infinite-scroll/-/redux-infinite-scroll-1.0.9.tgz",
       "integrity": "sha1-0Luiylp+a2qsQu7vX30ANsVqrx8="
     },
-    "redux-localstorage": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/redux-localstorage/-/redux-localstorage-0.4.1.tgz",
-      "integrity": "sha1-+vbXGcWBOXKU2BFHP/zt7gZckzw="
-    },
     "redux-promise-middleware": {
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/redux-promise-middleware/-/redux-promise-middleware-4.4.1.tgz",
diff --git a/package.json b/package.json
index c40275b0..33d0eea0 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,6 @@
     "redux": "^3.5.2",
     "redux-actions": "^0.12.0",
     "redux-infinite-scroll": "^1.0.9",
-    "redux-localstorage": "^0.4.1",
     "redux-promise-middleware": "^4.0.0",
     "redux-thunk": "^2.1.0",
     "remarkable": "^1.7.1",
diff --git a/src/Wrapper.js b/src/Wrapper.js
index 122657d0..37e231a0 100644
--- a/src/Wrapper.js
+++ b/src/Wrapper.js
@@ -9,7 +9,7 @@ import Cookie from 'js-cookie';
 import { getAuthenticatedUser, getLocale } from './reducers';
 
 import { login, logout } from './auth/authActions';
-import { getConfig, getRate, getTrendingTopics } from './app/appActions';
+import { getRate, getTrendingTopics } from './app/appActions';
 import Topnav from './components/Navigation/Topnav';
 import Transfer from './wallet/Transfer';
 import * as reblogActions from './app/Reblog/reblogActions';
@@ -24,7 +24,6 @@ import getTranslations, { getAvailableLocale } from './translations';
   {
     login,
     logout,
-    getConfig,
     getRate,
     getTrendingTopics,
     getRebloggedList: reblogActions.getRebloggedList,
@@ -38,7 +37,6 @@ export default class Wrapper extends React.PureComponent {
     history: PropTypes.shape().isRequired,
     login: PropTypes.func,
     logout: PropTypes.func,
-    getConfig: PropTypes.func,
     getRebloggedList: PropTypes.func,
     getRate: PropTypes.func,
     getTrendingTopics: PropTypes.func,
@@ -47,7 +45,6 @@ export default class Wrapper extends React.PureComponent {
   static defaultProps = {
     login: () => {},
     logout: () => {},
-    getConfig: () => {},
     getRebloggedList: () => {},
     getRate: () => {},
     getTrendingTopics: () => {},
@@ -57,7 +54,6 @@ export default class Wrapper extends React.PureComponent {
     if (Cookie.get('access_token')) {
       this.props.login();
     }
-    this.props.getConfig();
     this.props.getRebloggedList();
     this.props.getRate();
     this.props.getTrendingTopics();
diff --git a/src/app/AppSettings.js b/src/app/AppSettings.js
index 5b0fd1ee..0418dd44 100644
--- a/src/app/AppSettings.js
+++ b/src/app/AppSettings.js
@@ -1,100 +1,96 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
-import { FormattedMessage } from 'react-intl';
-import { getLocale } from '../reducers';
-import * as actions from './appActions';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { Select } from 'antd';
+import Loading from '../components/Icon/Loading';
+import { getLocale, getIsLocaleLoading, getIsReloading } from '../reducers';
+import { setLocale } from './appActions';
+import { reload } from '../auth/authActions';
 
+// NOTE: We are using injectIntl because without it locale won't update: see https://github.com/yahoo/react-intl/issues/371
+@injectIntl
 @connect(
   state => ({
+    reloading: getIsReloading(state),
     locale: getLocale(state),
+    localeLoading: getIsLocaleLoading(state),
   }),
-  dispatch =>
-    bindActionCreators(
-      {
-        setLocale: actions.setLocale,
-      },
-      dispatch,
-    ),
+  { reload, setLocale },
 )
 export default class AppSettings extends React.Component {
   static propTypes = {
+    reloading: PropTypes.bool,
+    locale: PropTypes.string,
+    localeLoading: PropTypes.bool,
     setLocale: PropTypes.func,
+    reload: PropTypes.func,
   };
 
   static defaultProps = {
+    reloading: false,
+    locale: 'auto',
+    localeLoading: false,
     setLocale: () => {},
+    reload: () => {},
   };
 
+  componentDidMount() {
+    this.props.reload();
+  }
+
+  languages = {
+    en: 'English',
+    zh: '简体中文',
+    cs: 'Čeština',
+    es: 'Español',
+    fr: 'Français',
+    pl: 'Polski',
+    de: 'Deutsch',
+    ru: 'Русский',
+    ko: '한국어',
+    nl: 'Nederlands',
+    sv: 'Svenska',
+  };
+
+  handleLocaleChange = locale => this.props.setLocale(locale);
+
   render() {
-    const { setLocale } = this.props;
+    const { reloading, locale, localeLoading } = this.props;
+
+    const languageOptions = [];
+
+    if (locale === 'auto') {
+      languageOptions.push(<Select.Option disabled value="auto">
+        <FormattedMessage key="auto" id="select_language" defaultMessage="Select your language" />
+      </Select.Option>);
+    }
+
+    Object.keys(this.languages).forEach((key) => {
+      languageOptions.push(
+        <Select.Option key={key} value={key}>
+          {this.languages[key]}
+        </Select.Option>);
+    });
+
     return (
       <div className="shifted">
         <div className="container">
           <h1>
             <FormattedMessage id="settings" defaultMessage="Settings" />
           </h1>
-          <h2>
-            <FormattedMessage id="language" defaultMessage="Language" />
-          </h2>
-          <div className="row my-4">
-            <div>
-              <a role="presentation" onClick={() => setLocale('en')}>
-                <h3>English</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('zh')}>
-                <h3>简体中文</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('cs')}>
-                <h3>Čeština</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('es')}>
-                <h3>Español</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('fr')}>
-                <h3>Français</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('pl')}>
-                <h3>Polski</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('de')}>
-                <h3>Deutsch</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('ru')}>
-                <h3>Русский</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('ko')}>
-                <h3>한국어</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('nl')}>
-                <h3>Nederlands</h3>
-              </a>
-            </div>
-            <div>
-              <a role="presentation" onClick={() => setLocale('sv')}>
-                <h3>Svenska</h3>
-              </a>
+          {reloading ? <Loading />
+            : <div>
+              <h2>
+                <FormattedMessage id="language" defaultMessage="Language" />
+              </h2>
+              <div>
+                <Select disabled={localeLoading} value={locale} style={{ width: '100%', maxWidth: 240 }} onChange={this.handleLocaleChange}>
+                  {languageOptions}
+                </Select>
+              </div>
             </div>
-          </div>
+          }
         </div>
       </div>
     );
diff --git a/src/app/appActions.js b/src/app/appActions.js
index bc4a834d..c9041f5e 100644
--- a/src/app/appActions.js
+++ b/src/app/appActions.js
@@ -1,43 +1,32 @@
 import Promise from 'bluebird';
-import { createAction } from 'redux-actions';
 import fetch from 'isomorphic-fetch';
+import { setLocaleMetadata } from '../helpers/metadata';
 
-export const CONFIG_REQUEST = '@app/CONFIG_REQUEST';
-export const CONFIG_SUCCESS = '@app/CONFIG_SUCCESS';
-export const CONFIG_FAILURE = '@app/CONFIG_FAILURE';
+export const GET_LOCALE = '@app/GET_LOCALE';
 
-export const FEED_REQUEST = '@app/FEED_REQUEST';
-export const FEED_SUCCESS = '@app/FEED_SUCCESS';
-export const FEED_FAILURE = '@app/FEED_FAILURE';
-export const CONTENT_REQUEST = '@app/CONTENT_REQUEST';
-export const CONTENT_SUCCESS = '@app/CONTENT_SUCCESS';
-export const ACCOUNT_REQUEST = '@app/ACCOUNT_REQUEST';
-export const ACCOUNT_SUCCESS = '@app/ACCOUNT_SUCCESS';
-
-export const GET_LOCALE = 'GET_LOCALE';
+export const SET_LOCALE = '@app/SET_LOCALE';
+export const SET_LOCALE_START = '@app/SET_LOCALE_START';
+export const SET_LOCALE_SUCCESS = '@app/SET_LOCALE_SUCCESS';
+export const SET_LOCALE_ERROR = '@app/SET_LOCALE_ERROR';
 
-export const getConfig = () =>
-  (dispatch, getState, { steemAPI }) => {
-    dispatch({ type: CONFIG_REQUEST });
-    steemAPI.getConfig((err, config) => {
-      dispatch({
-        type: CONFIG_SUCCESS,
-        config,
-      });
-    });
-  };
+export const GET_TRENDING_TOPICS = '@app/GET_TRENDING_TOPICS';
+export const GET_TRENDING_TOPICS_START = '@app/GET_TRENDING_TOPICS_START';
+export const GET_TRENDING_TOPICS_SUCCESS = '@app/GET_TRENDING_TOPICS_SUCCESS';
+export const GET_TRENDING_TOPICS_ERROR = '@app/GET_TRENDING_TOPICS_ERROR';
 
-export const SET_LOCALE = '@app/SET_LOCALE';
-export const setLocaleAction = createAction(SET_LOCALE);
+export const RATE_REQUEST = '@app/RATE_REQUEST';
+export const RATE_SUCCESS = '@app/RATE_SUCCESS';
 
 export const setLocale = locale =>
   (dispatch) => {
-    dispatch(setLocaleAction({ locale }));
+    dispatch({
+      type: SET_LOCALE,
+      payload: {
+        promise: setLocaleMetadata(locale),
+      },
+    });
   };
 
-export const RATE_REQUEST = '@app/RATE_REQUEST';
-export const RATE_SUCCESS = '@app/RATE_SUCCESS';
-
 export const getRate = () =>
   (dispatch) => {
     dispatch({ type: RATE_REQUEST });
@@ -52,11 +41,6 @@ export const getRate = () =>
       });
   };
 
-export const GET_TRENDING_TOPICS = '@app/GET_TRENDING_TOPICS';
-export const GET_TRENDING_TOPICS_START = '@app/GET_TRENDING_TOPICS_START';
-export const GET_TRENDING_TOPICS_SUCCESS = '@app/GET_TRENDING_TOPICS_SUCCESS';
-export const GET_TRENDING_TOPICS_ERROR = '@app/GET_TRENDING_TOPICS_ERROR';
-
 export const getTrendingTopics = () => (dispatch, getState, { steemAPI }) => {
   const getTrendingTagsAsync = Promise.promisify(steemAPI.getTrendingTags, { context: steemAPI });
   dispatch({
diff --git a/src/app/appReducer.js b/src/app/appReducer.js
index ba967ef7..23dffa93 100644
--- a/src/app/appReducer.js
+++ b/src/app/appReducer.js
@@ -1,11 +1,12 @@
 import * as appTypes from './appActions';
+import * as authActions from '../auth/authActions';
 import * as postActions from '../post/postActions';
 
 const initialState = {
   isFetching: false,
   isLoaded: false,
-  errorMessage: '',
   locale: 'auto',
+  localeLoading: false,
   rate: 0,
   trendingTopicsLoading: false,
   trendingTopics: [],
@@ -13,64 +14,37 @@ const initialState = {
 
 export default (state = initialState, action) => {
   switch (action.type) {
-    case appTypes.FEED_REQUEST:
-      return Object.assign({}, state, {
-        isFetching: true,
-        isLoaded: false,
-      });
-    case appTypes.FEED_SUCCESS:
-      return Object.assign({}, state, {
-        isFetching: false,
-        isLoaded: true,
-      });
-    case appTypes.FEED_FAILURE:
-      return Object.assign({}, state, {
-        isFetching: false,
-        isLoaded: true,
-      });
-    case appTypes.CONTENT_REQUEST:
-      return Object.assign({}, state, {
-        isFetching: true,
-        isLoaded: false,
-      });
-    case appTypes.CONTENT_SUCCESS:
-      return Object.assign({}, state, {
-        isFetching: false,
-        isLoaded: true,
-      });
-    case appTypes.ACCOUNT_REQUEST:
-      return Object.assign({}, state, {
-        isFetching: true,
-        isLoaded: false,
-      });
-    case appTypes.ACCOUNT_SUCCESS:
-      return Object.assign({}, state, {
-        isFetching: false,
-        isLoaded: true,
-      });
-    case appTypes.CONFIG_SUCCESS:
-      return Object.assign({}, state, {
-        config: action.config,
-      });
-
+    case authActions.LOGIN_SUCCESS:
+      return {
+        ...state,
+        locale: action.payload.user_metadata.locale || initialState.locale,
+      };
     case appTypes.GET_LOCALE:
       return {
         ...state,
         locale: action.payload.locale,
       };
-
-    case appTypes.SET_LOCALE:
+    case appTypes.SET_LOCALE_START:
       return {
         ...state,
-        locale: action.payload.locale,
+        localeLoading: true,
+      };
+    case appTypes.SET_LOCALE_SUCCESS:
+      return {
+        ...state,
+        locale: action.payload,
+        localeLoading: false,
+      };
+    case appTypes.SET_LOCALE_ERROR:
+      return {
+        ...state,
+        localeLoading: false,
       };
-
     case appTypes.RATE_SUCCESS:
       return {
         ...state,
         rate: action.rate,
       };
-
     case postActions.GET_CONTENT_START:
       return {
         ...state,
@@ -106,6 +80,7 @@ export default (state = initialState, action) => {
 };
 
 export const getLocale = state => state.locale;
+export const getIsLocaleLoading = state => state.localeLoading;
 export const getRate = state => state.rate;
 export const getIsTrendingTopicsLoading = state => state.trendingTopicsLoading;
 export const getTrendingTopics = state => state.trendingTopics;
diff --git a/src/auth/authActions.js b/src/auth/authActions.js
index 30cb1412..9d23abe1 100644
--- a/src/auth/authActions.js
+++ b/src/auth/authActions.js
@@ -7,44 +7,47 @@ import { initPushpad } from '../helpers/pushpadHelper';
 Promise.promisifyAll(steemConnect);
 
 export const LOGIN = '@auth/LOGIN';
-export const LOGIN_REQUEST = '@auth/LOGIN_START';
+export const LOGIN_START = '@auth/LOGIN_START';
 export const LOGIN_SUCCESS = '@auth/LOGIN_SUCCESS';
-export const LOGIN_FAILURE = '@auth/LOGIN_ERROR';
+export const LOGIN_ERROR = '@auth/LOGIN_ERROR';
+
+export const RELOAD = '@auth/RELOAD';
+export const RELOAD_START = '@auth/RELOAD_START';
+export const RELOAD_SUCCESS = '@auth/RELOAD_SUCCESS';
+export const RELOAD_ERROR = '@auth/RELOAD_ERROR';
+
 export const LOGOUT = '@auth/LOGOUT';
 export const LOGOUT_START = '@auth/LOGOUT_START';
 export const LOGOUT_ERROR = '@auth/LOGOUT_ERROR';
 export const LOGOUT_SUCCESS = '@auth/LOGOUT_SUCCESS';
 
-const requestLogin = () => ({ type: LOGIN_REQUEST });
-
-const loginSuccess = (user, token) => ({
-  type: LOGIN_SUCCESS,
-  user,
-  token,
-});
+export const login = () => (dispatch) => {
+  dispatch({
+    type: LOGIN,
+    payload: {
+      promise: steemConnect.me()
+        .then((resp) => {
+          dispatch(getFollowing(resp.user));
+          if (window.ga) {
+            window.ga('set', 'userId', resp.user);
+          }
 
-const loginFail = () => ({ type: LOGIN_FAILURE });
+          initPushpad(resp.user, Cookie.get('access_token'));
 
-export const login = () => (dispatch) => {
-  dispatch(requestLogin());
-  steemConnect.me((err, result) => {
-    if (err || !result || !result.user) {
-      dispatch(loginFail());
-      return;
-    }
-    dispatch(getFollowing(result.user));
-
-    const accessToken = Cookie.get('access_token');
-    dispatch(loginSuccess(result.account, accessToken));
-
-    if (window.ga) {
-      window.ga('set', 'userId', result.user);
-    }
-
-    initPushpad(result.user, accessToken);
+          return resp;
+        }),
+    },
   });
 };
 
+export const reload = () => dispatch =>
+  dispatch({
+    type: RELOAD,
+    payload: {
+      promise: steemConnect.me(),
+    },
+  });
+
 export const logout = () => (dispatch) => {
   dispatch({
     type: LOGOUT,
diff --git a/src/auth/authReducer.js b/src/auth/authReducer.js
index b0308019..a8576d8e 100644
--- a/src/auth/authReducer.js
+++ b/src/auth/authReducer.js
@@ -3,14 +3,14 @@ import * as types from './authActions';
 const initialState = {
   isAuthenticated: false,
   isFetching: false,
+  isReloading: false,
   loaded: false,
   user: {},
-  token: '',
 };
 
 export default (state = initialState, action) => {
   switch (action.type) {
-    case types.LOGIN_REQUEST:
+    case types.LOGIN_START:
       return {
         ...state,
         isFetching: true,
@@ -24,17 +24,25 @@ export default (state = initialState, action) => {
         isFetching: false,
         isAuthenticated: true,
         loaded: true,
-        errorMessage: '',
-        user: action.user,
-        token: action.token,
+        user: action.payload.account,
       };
-    case types.LOGIN_FAILURE:
+    case types.LOGIN_ERROR:
       return {
         ...state,
         isFetching: false,
         isAuthenticated: false,
         loaded: true,
-        errorMessage: action.message,
+      };
+    case types.RELOAD_START:
+      return {
+        ...state,
+        isReloading: true,
+      };
+    case types.RELOAD_SUCCESS:
+    case types.RELOAD_ERROR:
+      return {
+        ...state,
+        isReloading: false,
       };
     case types.LOGOUT_START:
       return {
@@ -49,7 +57,6 @@ export default (state = initialState, action) => {
         isFetching: false,
         loaded: true,
         user: {},
-        token: '',
       };
     default:
       return state;
@@ -59,5 +66,6 @@ export default (state = initialState, action) => {
 export const getIsAuthenticated = state => state.isAuthenticated;
 export const getIsAuthFetching = state => state.isFetching;
 export const getIsLoaded = state => state.loaded;
+export const getIsReloading = state => state.isReloading;
 export const getAuthenticatedUser = state => state.user;
 export const getAuthenticatedUserName = state => state.user.name;
diff --git a/src/bookmarks/Bookmarks.js b/src/bookmarks/Bookmarks.js
index d75e6fdb..b9e1902d 100644
--- a/src/bookmarks/Bookmarks.js
+++ b/src/bookmarks/Bookmarks.js
@@ -2,41 +2,53 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import { FormattedMessage } from 'react-intl';
-import { getFeed, getPosts } from '../reducers';
+import Loading from '../components/Icon/Loading';
+import { getFeed, getPosts, getPendingBookmarks, getIsReloading } from '../reducers';
 import Feed from '../feed/Feed';
 import {
   getFeedContentFromState,
   getFeedLoadingFromState,
   getFeedHasMoreFromState,
 } from '../helpers/stateHelpers';
+import { reload } from '../auth/authActions';
 import { getBookmarks } from './bookmarksActions';
 
 @connect(
   state => ({
     feed: getFeed(state),
     posts: getPosts(state),
-  }),
-  dispatch => ({
-    getBookmarks: () => dispatch(getBookmarks()),
-  }),
-)
+    pendingBookmarks: getPendingBookmarks(state),
+    reloading: getIsReloading(state),
+  }), { getBookmarks, reload })
 export default class Bookmarks extends React.Component {
   static propTypes = {
+    reloading: PropTypes.bool,
     feed: PropTypes.shape().isRequired,
     posts: PropTypes.shape().isRequired,
+    pendingBookmarks: PropTypes.arrayOf(PropTypes.number),
     getBookmarks: PropTypes.func,
+    reload: PropTypes.func,
   };
 
   static defaultProps = {
+    reloading: false,
+    pendingBookmarks: [],
     getBookmarks: () => {},
+    reload: () => {},
   };
 
-  componentDidMount() {
-    this.props.getBookmarks();
+  componentWillMount() {
+    this.props.reload().then(() => this.props.getBookmarks());
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.pendingBookmarks.length < this.props.pendingBookmarks.length) {
+      this.props.getBookmarks();
+    }
   }
 
   render() {
-    const { feed, posts } = this.props;
+    const { reloading, feed, posts } = this.props;
 
     const content = getFeedContentFromState('bookmarks', 'all', feed, posts);
     const isFetching = getFeedLoadingFromState('bookmarks', 'all', feed);
@@ -50,14 +62,15 @@ export default class Bookmarks extends React.Component {
           <h1 className="text-center">
             <FormattedMessage id="bookmarks" defaultMessage="Bookmarks" />
           </h1>
-          <Feed
+          {reloading && <Loading />}
+          {!reloading && <Feed
             content={content}
             isFetching={isFetching}
             hasMore={hasMore}
             loadContent={loadContentAction}
             loadMoreContent={loadMoreContentAction}
-          />
-          {!isFetching &&
+          />}
+          {!reloading && !isFetching &&
             !content.length &&
             <div className="container">
               <h3 className="text-center">
diff --git a/src/bookmarks/bookmarksActions.js b/src/bookmarks/bookmarksActions.js
index 45078d08..13e3d7d8 100644
--- a/src/bookmarks/bookmarksActions.js
+++ b/src/bookmarks/bookmarksActions.js
@@ -1,15 +1,15 @@
-import { createAction } from 'redux-actions';
+import { toggleBookmarkMetadata } from '../helpers/metadata';
+import { getBookmarks as getBookmarksSelector } from '../reducers';
 
 export const GET_BOOKMARKS = '@bookmarks/GET_BOOKMARKS';
 export const GET_BOOKMARKS_START = '@bookmarks/GET_BOOKMARKS_START';
 export const GET_BOOKMARKS_SUCCESS = '@bookmarks/GET_BOOKMARKS_SUCCESS';
-export const GET_BOOKMARKS_FAIL = '@bookmarks/GET_BOOKMARKS_FAIL';
+export const GET_BOOKMARKS_ERROR = '@bookmarks/GET_BOOKMARKS_ERROR';
 
-export const ADD_BOOKMARK = '@bookmarks/ADD_BOOKMARK';
-export const REMOVE_BOOKMARK = '@bookmarks/REMOVE_BOOKMARK';
-
-export const addBookmark = createAction(ADD_BOOKMARK);
-export const removeBookmark = createAction(REMOVE_BOOKMARK);
+export const TOGGLE_BOOKMARK = '@bookmarks/TOGGLE_BOOKMARK';
+export const TOGGLE_BOOKMARK_START = '@bookmarks/TOGGLE_BOOKMARK_START';
+export const TOGGLE_BOOKMARK_SUCCESS = '@bookmarks/TOGGLE_BOOKMARK_SUCCESS';
+export const TOGGLE_BOOKMARK_ERROR = '@bookmarks/TOGGLE_BOOKMARK_ERROR';
 
 /**
  * Use async await to load all the posts of bookmarked from steemAPI and returns a Promise
@@ -30,15 +30,12 @@ async function getBookmarksData(bookmarks, steemAPI) {
 }
 
 export const getBookmarks = () => (dispatch, getState, { steemAPI }) => {
-  const { auth, bookmarks } = getState();
-  const user = auth.isAuthenticated ? auth.user.name : 'guest';
-
-  const userBookmarks = bookmarks[user] || {};
+  const bookmarks = getBookmarksSelector(getState());
 
   dispatch({
     type: GET_BOOKMARKS,
     payload: {
-      promise: getBookmarksData(userBookmarks, steemAPI).then(bookmarksData => ({
+      promise: getBookmarksData(bookmarks, steemAPI).then(bookmarksData => ({
         postsData: bookmarksData,
       })),
       data: bookmarks,
@@ -46,13 +43,11 @@ export const getBookmarks = () => (dispatch, getState, { steemAPI }) => {
   });
 };
 
-export const toggleBookmark = (postId, author, permlink) => (dispatch, getState) => {
-  const { auth, bookmarks } = getState();
-  const user = auth.isAuthenticated ? auth.user.name : 'guest';
-
-  if (bookmarks[user] && bookmarks[user].filter(bookmark => bookmark.id === postId).length > 0) {
-    dispatch(removeBookmark({ user, postId }));
-  } else {
-    dispatch(addBookmark({ user, postId, author, permlink }));
-  }
-};
+export const toggleBookmark = (postId, author, permlink) => dispatch =>
+  dispatch({
+    type: TOGGLE_BOOKMARK,
+    payload: {
+      promise: toggleBookmarkMetadata(postId, author, permlink),
+    },
+    meta: { id: postId },
+  });
diff --git a/src/bookmarks/bookmarksReducer.js b/src/bookmarks/bookmarksReducer.js
index 7e7105ff..d2e84682 100644
--- a/src/bookmarks/bookmarksReducer.js
+++ b/src/bookmarks/bookmarksReducer.js
@@ -1,39 +1,33 @@
+import * as authActions from '../auth/authActions';
 import * as bookmarksActions from './bookmarksActions';
 
-const initialState = {};
+const initialState = {
+  list: {},
+  pendingBookmarks: [],
+};
 
 const bookmarks = (state = initialState, action) => {
   switch (action.type) {
-    case bookmarksActions.ADD_BOOKMARK:
-      if (state[action.payload.user]) {
-        return {
-          ...state,
-          [action.payload.user]: [
-            ...state[action.payload.user],
-            {
-              id: action.payload.postId,
-              author: action.payload.author,
-              permlink: action.payload.permlink,
-            },
-          ],
-        };
-      }
+    case authActions.LOGIN_SUCCESS:
+      return {
+        ...state,
+        list: action.payload.user_metadata.bookmarks || initialState.list,
+      };
+    case bookmarksActions.TOGGLE_BOOKMARK_START:
+      return {
+        ...state,
+        pendingBookmarks: [...state.pendingBookmarks, action.meta.id],
+      };
+    case bookmarksActions.TOGGLE_BOOKMARK_SUCCESS:
       return {
         ...state,
-        [action.payload.user]: [
-          {
-            id: action.payload.postId,
-            author: action.payload.author,
-            permlink: action.payload.permlink,
-          },
-        ],
+        list: action.payload || initialState.list,
+        pendingBookmarks: state.pendingBookmarks.filter(bookmark => bookmark !== action.meta.id),
       };
-    case bookmarksActions.REMOVE_BOOKMARK:
+    case bookmarksActions.TOGGLE_BOOKMARK_ERROR:
       return {
         ...state,
-        [action.payload.user]: state[action.payload.user].filter(
-          bookmark => bookmark.id !== action.payload.postId,
-        ),
+        pendingBookmarks: state.pendingBookmarks.filter(bookmark => bookmark !== action.meta.id),
       };
     default:
       return state;
@@ -42,4 +36,5 @@ const bookmarks = (state = initialState, action) => {
 
 export default bookmarks;
 
-export const getBookmarks = state => state;
+export const getBookmarks = state => state.list;
+export const getPendingBookmarks = state => state.pendingBookmarks;
diff --git a/src/components/Editor/Editor.js b/src/components/Editor/Editor.js
index 95038bf7..340977ee 100644
--- a/src/components/Editor/Editor.js
+++ b/src/components/Editor/Editor.js
@@ -22,6 +22,7 @@ class Editor extends React.Component {
     reward: PropTypes.string,
     upvote: PropTypes.bool,
     loading: PropTypes.bool,
+    saving: PropTypes.bool,
     onUpdate: PropTypes.func,
     onSubmit: PropTypes.func,
     onError: PropTypes.func,
@@ -37,6 +38,7 @@ class Editor extends React.Component {
     recentTopics: [],
     popularTopics: [],
     loading: false,
+    saving: false,
     onUpdate: () => {},
     onSubmit: () => {},
     onError: () => {},
@@ -348,7 +350,7 @@ class Editor extends React.Component {
 
   render() {
     const { getFieldDecorator } = this.props.form;
-    const { intl, loading } = this.props;
+    const { intl, loading, saving } = this.props;
 
     return (
       <Form className="Editor" layout="vertical" onSubmit={this.handleSubmit}>
@@ -450,17 +452,22 @@ class Editor extends React.Component {
             {' '}
             <FormattedMessage id="markdown_supported" defaultMessage="Styling with markdown supported" />
           </span>
-          <Form.Item className="Editor__bottom__submit">
-            <Action
-              primary
-              loading={loading}
-              disabled={loading}
-              text={intl.formatMessage({
-                id: loading ? 'post_send_progress' : 'post_send',
-                defaultMessage: loading ? 'Submitting' : 'Post',
-              })}
-            />
-          </Form.Item>
+          <div className="Editor__bottom__right">
+            {saving && <span className="Editor__bottom__right__saving">
+              <FormattedMessage id="saving" defaultMessage="Saving..." />
+            </span>}
+            <Form.Item className="Editor__bottom__submit">
+              <Action
+                primary
+                loading={loading}
+                disabled={loading}
+                text={intl.formatMessage({
+                  id: loading ? 'post_send_progress' : 'post_send',
+                  defaultMessage: loading ? 'Submitting' : 'Post',
+                })}
+              />
+            </Form.Item>
+          </div>
         </div>
       </Form>
     );
diff --git a/src/components/Editor/Editor.less b/src/components/Editor/Editor.less
index b64c79a7..f6bcb9d3 100644
--- a/src/components/Editor/Editor.less
+++ b/src/components/Editor/Editor.less
@@ -77,6 +77,15 @@
       }
     }
 
+    &__right {
+      display: flex;
+      align-items: center;
+
+      &__saving {
+        margin-right: 16px;
+      }
+    }
+
     &__submit {
       margin-bottom: 0 !important;
 
diff --git a/src/components/Story/Story.js b/src/components/Story/Story.js
index a358fe21..19532259 100644
--- a/src/components/Story/Story.js
+++ b/src/components/Story/Story.js
@@ -19,6 +19,7 @@ class Story extends React.Component {
     postState: PropTypes.shape().isRequired,
     pendingLike: PropTypes.bool,
     pendingFollow: PropTypes.bool,
+    pendingBookmark: PropTypes.bool,
     onFollowClick: PropTypes.func,
     onSaveClick: PropTypes.func,
     onReportClick: PropTypes.func,
@@ -29,6 +30,7 @@ class Story extends React.Component {
   static defaultProps = {
     pendingLike: false,
     pendingFollow: false,
+    pendingBookmark: false,
     onFollowClick: () => {},
     onSaveClick: () => {},
     onReportClick: () => {},
@@ -59,6 +61,7 @@ class Story extends React.Component {
       postState,
       pendingLike,
       pendingFollow,
+      pendingBookmark,
       onLikeClick,
       onShareClick,
     } = this.props;
@@ -109,7 +112,7 @@ class Story extends React.Component {
                   {followText}
                 </PopoverMenuItem>
                 <PopoverMenuItem key="save">
-                  <i className="iconfont icon-collection" />
+                  {pendingBookmark ? <Icon type="loading" /> : <i className="iconfont icon-collection" />}
                   <FormattedMessage id={postState.isSaved ? 'unsave_post' : 'save_post'} defaultMessage={postState.isSaved ? 'Unsave post' : 'Save post'} />
                 </PopoverMenuItem>
                 <PopoverMenuItem key="report">
diff --git a/src/components/Story/StoryFull.js b/src/components/Story/StoryFull.js
index 2262c14b..aed6750b 100644
--- a/src/components/Story/StoryFull.js
+++ b/src/components/Story/StoryFull.js
@@ -21,6 +21,7 @@ class StoryFull extends React.Component {
     postState: PropTypes.shape().isRequired,
     pendingLike: PropTypes.bool,
     pendingFollow: PropTypes.bool,
+    pendingBookmark: PropTypes.bool,
     commentCount: PropTypes.number,
     onFollowClick: PropTypes.func,
     onSaveClick: PropTypes.func,
@@ -32,6 +33,7 @@ class StoryFull extends React.Component {
   static defaultProps = {
     pendingLike: false,
     pendingFollow: false,
+    pendingBookmark: false,
     commentCount: 0,
     onFollowClick: () => {},
     onSaveClick: () => {},
@@ -97,6 +99,7 @@ class StoryFull extends React.Component {
       postState,
       pendingLike,
       pendingFollow,
+      pendingBookmark,
       commentCount,
       onLikeClick,
       onShareClick,
@@ -192,7 +195,7 @@ class StoryFull extends React.Component {
                   {followText}
                 </PopoverMenuItem>
                 <PopoverMenuItem key="save">
-                  <i className="iconfont icon-collection" />
+                  {pendingBookmark ? <Icon type="loading" /> : <i className="iconfont icon-collection" />}
                   <FormattedMessage
                     id={postState.isSaved ? 'unsave_post' : 'save_post'}
                     defaultMessage={postState.isSaved ? 'Unsave post' : 'Save post'}
diff --git a/src/errorMiddleware.js b/src/errorMiddleware.js
index 6389d516..46cba618 100644
--- a/src/errorMiddleware.js
+++ b/src/errorMiddleware.js
@@ -8,6 +8,10 @@ export default function errorMiddleware({ dispatch }) {
   return next => (action) => {
     if (action.type && action.type.match(/error/i) && action.payload instanceof Error) {
       if (action.payload && action.payload.error_description) {
+        // Don't display error message for invalid_grant SDK error
+        if (action.payload.error === 'invalid_grant') {
+          return next(action);
+        }
         dispatch(notify(parseBlockChainError(action.payload.error_description), 'error'));
       }
     }
diff --git a/src/feed/Feed.js b/src/feed/Feed.js
index 527de341..7910bfa1 100644
--- a/src/feed/Feed.js
+++ b/src/feed/Feed.js
@@ -11,6 +11,7 @@ import { followUser, unfollowUser } from '../user/userActions';
 import {
   getAuthenticatedUser,
   getBookmarks,
+  getPendingBookmarks,
   getPendingLikes,
   getRebloggedList,
   getPendingReblogs,
@@ -25,6 +26,7 @@ import StoryLoading from '../components/Story/StoryLoading';
   state => ({
     user: getAuthenticatedUser(state),
     bookmarks: getBookmarks(state),
+    pendingBookmarks: getPendingBookmarks(state),
     pendingLikes: getPendingLikes(state),
     reblogList: getRebloggedList(state),
     pendingReblogs: getPendingReblogs(state),
@@ -47,6 +49,7 @@ export default class Feed extends React.Component {
     pendingFollows: PropTypes.arrayOf(PropTypes.string).isRequired,
     pendingReblogs: PropTypes.arrayOf(PropTypes.number).isRequired,
     bookmarks: PropTypes.shape().isRequired,
+    pendingBookmarks: PropTypes.arrayOf(PropTypes.number).isRequired,
     followingList: PropTypes.arrayOf(PropTypes.string).isRequired,
     reblogList: PropTypes.arrayOf(PropTypes.number).isRequired,
     isFetching: PropTypes.bool,
@@ -87,6 +90,7 @@ export default class Feed extends React.Component {
       hasMore,
       pendingLikes,
       bookmarks,
+      pendingBookmarks,
       reblogList,
       followingList,
       pendingFollows,
@@ -120,10 +124,7 @@ export default class Feed extends React.Component {
           const postState = {
             isReblogged: reblogList.includes(post.id),
             isReblogging: pendingReblogs.includes(post.id),
-            isSaved:
-              bookmarks[user.name] &&
-              bookmarks[user.name].filter(bookmark => bookmark.id === post.id).length >
-                0,
+            isSaved: !!bookmarks[post.id],
             isLiked: userVote.percent > 0,
             isReported: userVote.percent < 0,
             userFollowed: followingList.includes(post.author),
@@ -142,6 +143,7 @@ export default class Feed extends React.Component {
               postState={postState}
               pendingLike={pendingLikes.includes(post.id)}
               pendingFollow={pendingFollows.includes(post.author)}
+              pendingBookmark={pendingBookmarks.includes(post.id)}
               onFollowClick={this.handleFollowClick}
               onSaveClick={() => toggleBookmark(post.id, post.author, post.permlink)}
               onReportClick={reportPost}
diff --git a/src/helpers/metadata.js b/src/helpers/metadata.js
new file mode 100644
index 00000000..a5f2a941
--- /dev/null
+++ b/src/helpers/metadata.js
@@ -0,0 +1,50 @@
+import Promise from 'bluebird';
+import SteemConnect from 'sc2-sdk';
+import omit from 'lodash/omit';
+
+Promise.promisifyAll(SteemConnect, { context: SteemConnect });
+
+const getMetadata = () => SteemConnect.me().then(resp => resp.user_metadata);
+
+export const setLocaleMetadata = locale => getMetadata()
+  .then(metadata =>
+    SteemConnect.updateUserMetadata({
+      ...metadata,
+      locale,
+    }),
+  )
+  .then(resp => resp.user_metadata.locale);
+
+export const addDraftMetadata = draft => getMetadata()
+  .then(metadata =>
+    SteemConnect.updateUserMetadata({
+      ...metadata,
+      drafts: {
+        ...metadata.drafts,
+        [draft.id]: draft.postData,
+      },
+    }),
+  )
+  .then(resp => resp.user_metadata.drafts[draft.id]);
+
+export const deleteDraftMetadata = draftId => getMetadata()
+  .then(metadata =>
+    SteemConnect.updateUserMetadata({
+      ...metadata,
+      drafts: omit(metadata.drafts, draftId),
+    }),
+  )
+  .then(resp => resp.user_metadata.drafts);
+
+export const toggleBookmarkMetadata = (id, author, permlink) => getMetadata()
+  .then(metadata =>
+    SteemConnect.updateUserMetadata({
+      ...metadata,
+      bookmarks: (metadata.bookmarks && metadata.bookmarks[id])
+        ? omit(metadata.bookmarks, id)
+        : { ...metadata.bookmarks, [id]: { id, author, permlink } },
+    }),
+  )
+  .then(resp => resp.user_metadata.bookmarks);
+
+export default getMetadata;
diff --git a/src/locales/en.json b/src/locales/en.json
index 624b69b1..017d7bd1 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -80,6 +80,7 @@
   "show_more":"View more",
   "show_less":"View less",
   "language":"Language",
+  "select_language":"Select your language",
   "discover_more_people":"Discover More People",
   "see_all_comments":"See All Comments",
   "see_all_recommendations":"See All Recommendations",
@@ -126,6 +127,7 @@
   "reward_option_50":"50% SBD and 50% SP",
   "reward_option_0":"Declined",
   "like_post":"Like this post",
+  "saving":"Saving...",
   "post_send":"Post",
   "post_send_progress":"Submitting",
   "heading_1":"Heading 1",
diff --git a/src/post/PostContent.js b/src/post/PostContent.js
index 3445decf..c27fa7fd 100644
--- a/src/post/PostContent.js
+++ b/src/post/PostContent.js
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-html';
 import {
   getAuthenticatedUser,
   getBookmarks,
+  getPendingBookmarks,
   getPendingLikes,
   getRebloggedList,
   getPendingReblogs,
@@ -25,6 +26,7 @@ import StoryFull from '../components/Story/StoryFull';
   state => ({
     user: getAuthenticatedUser(state),
     bookmarks: getBookmarks(state),
+    pendingBookmarks: getPendingBookmarks(state),
     pendingLikes: getPendingLikes(state),
     reblogList: getRebloggedList(state),
     pendingReblogs: getPendingReblogs(state),
@@ -50,6 +52,7 @@ class PostContent extends React.Component {
     followingList: PropTypes.arrayOf(PropTypes.string),
     pendingFollows: PropTypes.arrayOf(PropTypes.string),
     bookmarks: PropTypes.shape(),
+    pendingBookmarks: PropTypes.arrayOf(PropTypes.number).isRequired,
     toggleBookmark: PropTypes.func,
     votePost: PropTypes.func,
     reblog: PropTypes.func,
@@ -125,6 +128,7 @@ class PostContent extends React.Component {
       followingList,
       pendingFollows,
       bookmarks,
+      pendingBookmarks,
     } = this.props;
 
     const postMetaData = jsonParse(content.json_metadata);
@@ -139,9 +143,7 @@ class PostContent extends React.Component {
     const postState = {
       isReblogged: reblogList.includes(content.id),
       isReblogging: pendingReblogs.includes(content.id),
-      isSaved:
-        bookmarks[user.name] &&
-        bookmarks[user.name].filter(bookmark => bookmark.id === content.id).length > 0,
+      isSaved: !!bookmarks[content.id],
       isLiked: userVote.percent > 0,
       isReported: userVote.percent < 0,
       userFollowed: followingList.includes(content.author),
@@ -188,6 +190,7 @@ class PostContent extends React.Component {
           commentCount={content.children}
           pendingLike={pendingLikes.includes(content.id)}
           pendingFollow={pendingFollows.includes(content.author)}
+          pendingBookmark={pendingBookmarks.includes(content.id)}
           onLikeClick={this.handleLikeClick}
           onReportClick={this.handleReportClick}
           onShareClick={this.handleShareClick}
diff --git a/src/post/Write/DraftRow.js b/src/post/Write/DraftRow.js
index 10c1b303..ad3e6545 100644
--- a/src/post/Write/DraftRow.js
+++ b/src/post/Write/DraftRow.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Button } from 'antd';
 import { FormattedMessage } from 'react-intl';
 import { connect } from 'react-redux';
 import { Link } from 'react-router-dom';
@@ -10,15 +11,19 @@ class DraftRow extends React.Component {
   static propTypes = {
     id: PropTypes.string.isRequired,
     data: PropTypes.shape().isRequired,
+    pending: PropTypes.bool,
     deleteDraft: PropTypes.func,
   };
 
   static defaultProps = {
+    pending: false,
     deleteDraft: () => {},
   };
 
+  handleDeleteClick = () => this.props.deleteDraft(this.props.id);
+
   render() {
-    const { id, data } = this.props;
+    const { id, data, pending } = this.props;
     let { title = '', body = '' } = data;
     title = title.trim();
     body = body.replace(/\r?\n|\r|[\u200B-\u200D\uFEFF]/g, ' ').substring(0, 50);
@@ -33,14 +38,9 @@ class DraftRow extends React.Component {
           </h3>
         </Link>
         <div>
-          <a
-            role="presentation"
-            onClick={() => {
-              this.props.deleteDraft(id);
-            }}
-          >
+          <Button loading={pending} type="danger" onClick={this.handleDeleteClick}>
             <FormattedMessage id="draft_delete" defaultMessage="Delete this draft" />
-          </a>
+          </Button>
         </div>
       </div>
     );
diff --git a/src/post/Write/Drafts.js b/src/post/Write/Drafts.js
index ad0e5a0a..700028e3 100644
--- a/src/post/Write/Drafts.js
+++ b/src/post/Write/Drafts.js
@@ -3,31 +3,50 @@ import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
 import { connect } from 'react-redux';
 import _ from 'lodash';
-
-import { getDraftPosts } from '../../reducers';
+import Loading from '../../components/Icon/Loading';
+import { reload } from '../../auth/authActions';
+import { getDraftPosts, getPendingDrafts, getIsReloading } from '../../reducers';
 
 import DraftRow from './DraftRow';
 
 @connect(state => ({
+  reloading: getIsReloading(state),
   draftPosts: getDraftPosts(state),
-}))
+  pendingDrafts: getPendingDrafts(state),
+}), { reload })
 class Drafts extends React.Component {
   static propTypes = {
+    reloading: PropTypes.bool,
     draftPosts: PropTypes.shape().isRequired,
+    pendingDrafts: PropTypes.arrayOf(PropTypes.string),
+    reload: PropTypes.func,
+  };
+
+  static defaultProps = {
+    reloading: false,
+    pendingDrafts: [],
+    reload: () => {},
   };
 
+  componentDidMount() {
+    this.props.reload();
+  }
+
   render() {
-    const { draftPosts } = this.props;
+    const { reloading, draftPosts, pendingDrafts } = this.props;
 
     return (
       <div className="shifted">
         <div className="container">
           <h1><FormattedMessage id="drafts" defaultMessage="Drafts" /></h1>
-          {_.size(draftPosts) === 0 &&
+          {reloading && <Loading />}
+          {!reloading && _.size(draftPosts) === 0 &&
             <h3 className="text-center">
               <FormattedMessage id="drafts_empty" defaultMessage="You don't have any draft saved" />
             </h3>}
-          {_.map(draftPosts, (draft, key) => <DraftRow key={key} data={draft.postData} id={key} />)}
+          {!reloading && _.map(draftPosts, (draft, key) =>
+            <DraftRow key={key} data={draft} id={key} pending={pendingDrafts.includes(key)} />)
+          }
         </div>
       </div>
     );
diff --git a/src/post/Write/Write.js b/src/post/Write/Write.js
index 51931af9..67a1d4cb 100644
--- a/src/post/Write/Write.js
+++ b/src/post/Write/Write.js
@@ -8,7 +8,7 @@ import isArray from 'lodash/isArray';
 import 'url-search-params-polyfill';
 import GetBoost from '../../components/Sidebar/GetBoost';
 
-import { getAuthenticatedUser, getDraftPosts, getIsEditorLoading } from '../../reducers';
+import { getAuthenticatedUser, getDraftPosts, getIsEditorLoading, getIsEditorSaving } from '../../reducers';
 
 import { createPost, saveDraft, newPost } from './editorActions';
 import { notify } from '../../app/Notification/notificationActions';
@@ -23,6 +23,7 @@ const version = require('../../../package.json').version;
     user: getAuthenticatedUser(state),
     draftPosts: getDraftPosts(state),
     loading: getIsEditorLoading(state),
+    saving: getIsEditorSaving(state),
   }),
   {
     createPost,
@@ -36,6 +37,7 @@ class Write extends React.Component {
     user: PropTypes.shape().isRequired,
     draftPosts: PropTypes.shape().isRequired,
     loading: PropTypes.bool.isRequired,
+    saving: PropTypes.bool,
     location: PropTypes.shape().isRequired,
     newPost: PropTypes.func,
     createPost: PropTypes.func,
@@ -44,6 +46,7 @@ class Write extends React.Component {
   };
 
   static defaultProps = {
+    saving: false,
     newPost: () => {},
     createPost: () => {},
     saveDraft: () => {},
@@ -66,21 +69,20 @@ class Write extends React.Component {
     const { draftPosts, location: { search } } = this.props;
     const draftId = new URLSearchParams(search).get('draft');
     const draftPost = draftPosts[draftId];
-    const postData = draftPost && draftPost.postData;
 
-    if (postData) {
-      const { jsonMetadata } = postData;
+    if (draftPost) {
+      const { jsonMetadata } = draftPost;
       let tags = [];
       if (isArray(jsonMetadata.tags)) {
         tags = jsonMetadata.tags;
       }
       // eslint-disable-next-line
       this.setState({
-        initialTitle: postData.title || '',
+        initialTitle: draftPost.title || '',
         initialTopics: tags || [],
-        initialBody: postData.body || '',
-        initialReward: postData.reward || '50',
-        initialUpvote: postData.upvote,
+        initialBody: draftPost.body || '',
+        initialReward: draftPost.reward || '50',
+        initialUpvote: draftPost.upvote,
       });
     }
   }
@@ -207,7 +209,7 @@ class Write extends React.Component {
 
   render() {
     const { initialTitle, initialTopics, initialBody, initialReward, initialUpvote } = this.state;
-    const { loading } = this.props;
+    const { loading, saving } = this.props;
 
     return (
       <div className="shifted">
@@ -220,6 +222,7 @@ class Write extends React.Component {
           <div className="center">
             <Editor
               ref={this.setForm}
+              saving={saving}
               title={initialTitle}
               topics={initialTopics}
               body={initialBody}
diff --git a/src/post/Write/editorActions.js b/src/post/Write/editorActions.js
index f15df76f..49a51a2c 100644
--- a/src/post/Write/editorActions.js
+++ b/src/post/Write/editorActions.js
@@ -3,6 +3,7 @@ import assert from 'assert';
 import SteemConnect from 'sc2-sdk';
 import { push } from 'react-router-redux';
 import { createAction } from 'redux-actions';
+import { addDraftMetadata, deleteDraftMetadata } from '../../helpers/metadata';
 import { jsonParse } from '../../helpers/formatter';
 import { createPermlink, getBodyPatchIfSmaller } from '../../vendor/steemitHelpers';
 
@@ -15,21 +16,39 @@ export const NEW_POST = '@editor/NEW_POST';
 export const newPost = createAction(NEW_POST);
 
 export const SAVE_DRAFT = '@editor/SAVE_DRAFT';
+export const SAVE_DRAFT_START = '@editor/SAVE_DRAFT_START';
+export const SAVE_DRAFT_SUCCESS = '@editor/SAVE_DRAFT_SUCCESS';
+export const SAVE_DRAFT_ERROR = '@editor/SAVE_DRAFT_ERROR';
 
 export const DELETE_DRAFT = '@editor/DELETE_DRAFT';
-export const deleteDraft = createAction(DELETE_DRAFT);
+export const DELETE_DRAFT_START = '@editor/DELETE_DRAFT_START';
+export const DELETE_DRAFT_SUCCESS = '@editor/DELETE_DRAFT_SUCCESS';
+export const DELETE_DRAFT_ERROR = '@editor/DELETE_DRAFT_ERROR';
 
 export const saveDraft = (post, redirect) => (dispatch) => {
   dispatch({
     type: SAVE_DRAFT,
     payload: {
-      ...post,
+      promise: addDraftMetadata(post)
+        .then((resp) => {
+          if (redirect) {
+            dispatch(push(`/write?draft=${post.id}`));
+          }
+          return resp;
+        }),
     },
+    meta: { postId: post.id },
   });
+};
 
-  if (redirect) {
-    dispatch(push(`/write?draft=${post.id}`));
-  }
+export const deleteDraft = draftId => (dispatch) => {
+  dispatch({
+    type: DELETE_DRAFT,
+    payload: {
+      promise: deleteDraftMetadata(draftId),
+    },
+    meta: { id: draftId },
+  });
 };
 
 export const editPost = post => (dispatch) => {
diff --git a/src/post/Write/editorReducer.js b/src/post/Write/editorReducer.js
index 627b8be1..a3db2cb5 100644
--- a/src/post/Write/editorReducer.js
+++ b/src/post/Write/editorReducer.js
@@ -1,4 +1,4 @@
-import omit from 'lodash/omit';
+import * as authActions from '../../auth/authActions';
 import * as editorActions from './editorActions';
 import * as userActions from '../../user/userActions';
 
@@ -6,11 +6,18 @@ const defaultState = {
   loading: false,
   error: null,
   success: false,
+  saving: false,
   draftPosts: {},
+  pendingDrafts: [],
 };
 
 const editor = (state = defaultState, action) => {
   switch (action.type) {
+    case authActions.LOGIN_SUCCESS:
+      return {
+        ...state,
+        draftPosts: action.payload.user_metadata.drafts || defaultState.draftPosts,
+      };
     case editorActions.NEW_POST:
       return {
         ...state,
@@ -40,20 +47,42 @@ const editor = (state = defaultState, action) => {
         loading: false,
         success: true,
       };
-    case editorActions.SAVE_DRAFT: {
-      const { postData } = action.payload;
-      const id = action.payload.id;
+    case editorActions.SAVE_DRAFT_START:
       return {
         ...state,
-        draftPosts: { ...state.draftPosts, [id]: { postData } },
+        saving: true,
       };
-    }
-    case editorActions.DELETE_DRAFT: {
+    case editorActions.SAVE_DRAFT_SUCCESS:
+      return {
+        ...state,
+        draftPosts: { ...state.draftPosts, [action.meta.postId]: action.payload },
+        saving: false,
+      };
+    case editorActions.SAVE_DRAFT_ERROR:
+      return {
+        ...state,
+        saving: false,
+      };
+    case editorActions.DELETE_DRAFT_START:
+      return {
+        ...state,
+        pendingDrafts: [
+          ...state.pendingDrafts,
+          action.meta.id,
+        ],
+      };
+    case editorActions.DELETE_DRAFT_SUCCESS: {
       return {
         ...state,
-        draftPosts: omit(state.draftPosts, action.payload),
+        draftPosts: action.payload,
+        pendingDrafts: state.pendingDrafts.filter(id => id !== action.meta.id),
       };
     }
+    case editorActions.DELETE_DRAFT_ERROR:
+      return {
+        ...state,
+        pendingDrafts: state.pendingDrafts.filter(id => id !== action.meta.id),
+      };
     case userActions.UPLOAD_FILE_START:
       return { ...state, loadingImg: true };
 
@@ -69,3 +98,5 @@ export default editor;
 
 export const getDraftPosts = state => state.draftPosts;
 export const getIsEditorLoading = state => state.loading;
+export const getIsEditorSaving = state => state.saving;
+export const getPendingDrafts = state => state.pendingDrafts;
diff --git a/src/reducers.js b/src/reducers.js
index 8b942a27..5ec729d1 100644
--- a/src/reducers.js
+++ b/src/reducers.js
@@ -40,6 +40,7 @@ export default reducers;
 export const getIsAuthenticated = state => fromAuth.getIsAuthenticated(state.auth);
 export const getIsAuthFetching = state => fromAuth.getIsAuthFetching(state.auth);
 export const getIsLoaded = state => fromAuth.getIsLoaded(state.auth);
+export const getIsReloading = state => fromAuth.getIsReloading(state.auth);
 export const getAuthenticatedUser = state => fromAuth.getAuthenticatedUser(state.auth);
 export const getAuthenticatedUserName = state => fromAuth.getAuthenticatedUserName(state.auth);
 
@@ -50,8 +51,11 @@ export const getPendingLikes = state => fromPosts.getPendingLikes(state.posts);
 
 export const getDraftPosts = state => fromEditor.getDraftPosts(state.editor);
 export const getIsEditorLoading = state => fromEditor.getIsEditorLoading(state.editor);
+export const getIsEditorSaving = state => fromEditor.getIsEditorSaving(state.editor);
+export const getPendingDrafts = state => fromEditor.getPendingDrafts(state.editor);
 
 export const getLocale = state => fromApp.getLocale(state.app);
+export const getIsLocaleLoading = state => fromApp.getIsLocaleLoading(state.app);
 export const getRate = state => fromApp.getRate(state.app);
 export const getIsTrendingTopicsLoading = state => fromApp.getIsTrendingTopicsLoading(state.app);
 export const getTrendingTopics = state => fromApp.getTrendingTopics(state.app);
@@ -65,6 +69,7 @@ export const getCommentsPendingVotes = state =>
   fromComments.getCommentsPendingVotes(state.comments);
 
 export const getBookmarks = state => fromBookmarks.getBookmarks(state.bookmarks);
+export const getPendingBookmarks = state => fromBookmarks.getPendingBookmarks(state.bookmarks);
 
 export const getRebloggedList = state => fromReblog.getRebloggedList(state.reblog);
 export const getPendingReblogs = state => fromReblog.getPendingReblogs(state.reblog);
diff --git a/src/store.js b/src/store.js
index 099bb2f7..71ee6105 100644
--- a/src/store.js
+++ b/src/store.js
@@ -1,7 +1,5 @@
 import promiseMiddleware from 'redux-promise-middleware';
-import persistState from 'redux-localstorage';
 import thunk from 'redux-thunk';
-import { pick } from 'lodash/object';
 import { applyMiddleware, createStore, compose } from 'redux';
 import { routerMiddleware } from 'react-router-redux';
 import api from './steemAPI';
@@ -41,17 +39,7 @@ let enhancer;
 if (process.env.IS_BROWSER) {
   // eslint-disable-next-line no-underscore-dangle
   const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
-  enhancer = composeEnhancers(
-    applyMiddleware(...middleware),
-    persistState(['app', 'favorites', 'editor', 'bookmarks'], {
-      slicer: () => state => ({
-        app: pick(state.app, ['locale']),
-        bookmarks: state.bookmarks,
-        favorites: state.favorites,
-        editor: pick(state.editor, ['draftPosts']),
-      }),
-    }),
-  );
+  enhancer = composeEnhancers(applyMiddleware(...middleware));
 } else {
   enhancer = compose(applyMiddleware(...middleware));
 }
diff --git a/src/translations/index.js b/src/translations/index.js
index c8e55f96..81f7ca77 100644
--- a/src/translations/index.js
+++ b/src/translations/index.js
@@ -39,18 +39,29 @@ export const translations = {
   pl: plTranslations,
 };
 
+export const getBrowserLocale = () => {
+  let detectedLocale;
+  if (typeof navigator !== 'undefined') {
+    detectedLocale =
+      navigator.userLanguage ||
+      navigator.language ||
+      (navigator.languages && navigator.languages[0] ? navigator.languages[0] : undefined);
+  }
+  if (detectedLocale) {
+    return detectedLocale.slice(0, 2);
+  }
+  return undefined;
+};
+
 export const getAvailableLocale = (appLocale) => {
   let locale = appLocale || 'auto';
 
-  if (typeof navigator !== 'undefined' && appLocale === 'auto') {
-    locale =
-      navigator.userLanguage ||
-      navigator.language ||
-      (navigator.languages && navigator.languages[0] ? navigator.languages[0] : 'en');
+  if (appLocale === 'auto') {
+    locale = getBrowserLocale() || 'en';
   }
 
-  if (translations[locale.slice(0, 2)]) {
-    return locale.slice(0, 2);
+  if (translations[locale]) {
+    return locale;
   }
 
   return 'en';
diff --git a/webpack/makeConfig.js b/webpack/makeConfig.js
index 719d39f9..9020ea52 100644
--- a/webpack/makeConfig.js
+++ b/webpack/makeConfig.js
@@ -55,7 +55,12 @@ function makePlugins(options) {
         BUSYPUSH_ENDPOINT: process.env.BUSYPUSH_ENDPOINT,
       },
     }),
-    new LodashModuleReplacementPlugin({ collections: true, paths: true, shorthands: true }),
+    new LodashModuleReplacementPlugin({
+      collections: true,
+      paths: true,
+      shorthands: true,
+      flattening: true,
+    }),
     new Visualizer({
       filename: './statistics.html',
     }),


Test patch:
diff --git a/src/auth/__tests__/authReducers.js b/src/auth/__tests__/authReducers.js
index f882075d..0b7c2873 100644
--- a/src/auth/__tests__/authReducers.js
+++ b/src/auth/__tests__/authReducers.js
@@ -5,9 +5,9 @@ import authReducer from '../authReducer';
 const initialStateMock = {
   isAuthenticated: false,
   isFetching: false,
+  isReloading: false,
   loaded: false,
   user: {},
-  token: '',
 };
 
 describe('authReducer', () => {
@@ -18,14 +18,14 @@ describe('authReducer', () => {
     expect(authReducer(undefined, {})).to.deep.equal(initialStateMock);
   });
 
-  describe(`dispatching action ${authTypes.LOGIN_REQUEST}`, () => {
+  describe(`dispatching action ${authTypes.LOGIN_START}`, () => {
     it('is expected to return isFetching as true', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_REQUEST })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_START })).to.have
         .property('isFetching')
         .and.equal(true);
     });
     it('is expected to return isAuthenticated as false', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_REQUEST })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_START })).to.have
         .property('isAuthenticated')
         .and.equal(false);
     });
@@ -33,40 +33,33 @@ describe('authReducer', () => {
 
   describe(`dispatching action ${authTypes.LOGIN_SUCCESS}`, () => {
     it('is expected to return isFetching as false', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_SUCCESS })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_SUCCESS, payload: { account: 'testUser' } })).to.have
         .property('isFetching')
         .and.equal(false);
     });
     it('is expected to return isAuthenticated as true', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_SUCCESS })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_SUCCESS, payload: { account: 'testUser' } })).to.have
         .property('isAuthenticated')
         .and.equal(true);
     });
     it('is expected to return user from the action', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_SUCCESS, user: 'testUser' })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_SUCCESS, payload: { account: 'testUser' } })).to.have
         .property('user')
         .and.equal('testUser');
     });
   });
 
-  describe(`dispatching action ${authTypes.LOGIN_FAILURE}`, () => {
+  describe(`dispatching action ${authTypes.LOGIN_ERROR}`, () => {
     it('is expected to return isFetching as false', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_FAILURE })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_ERROR })).to.have
         .property('isFetching')
         .and.equal(false);
     });
     it('is expected to return isAuthenticated as false', () => {
-      expect(authReducer(undefined, { type: authTypes.LOGIN_FAILURE })).to.have
+      expect(authReducer(undefined, { type: authTypes.LOGIN_ERROR })).to.have
         .property('isAuthenticated')
         .and.equal(false);
     });
-    it('is expected to return errorMessage from action.message', () => {
-      expect(
-        authReducer(undefined, { type: authTypes.LOGIN_FAILURE, message: 'testMessage' }),
-      ).to.have
-        .property('errorMessage')
-        .and.equal('testMessage');
-    });
   });
 
   describe(`dispatching action ${authTypes.LOGOUT_SUCCESS}`, () => {
diff --git a/src/stories/__snapshots__/stories.test.js.snap b/src/stories/__snapshots__/stories.test.js.snap
index 099336fc..f401c0c4 100644
--- a/src/stories/__snapshots__/stories.test.js.snap
+++ b/src/stories/__snapshots__/stories.test.js.snap
@@ -576,23 +576,27 @@ exports[`Storyshots Editor Editor 1`] = `
         </span>
       </span>
       <div
-        className="ant-row ant-form-item Editor__bottom__submit"
-        style={undefined}
+        className="Editor__bottom__right"
       >
         <div
-          className="ant-form-item-control-wrapper"
+          className="ant-row ant-form-item Editor__bottom__submit"
+          style={undefined}
         >
           <div
-            className="ant-form-item-control "
+            className="ant-form-item-control-wrapper"
           >
-            <button
-              className="Action ant-btn-lg Action--primary"
-              disabled={false}
-              onClick={[Function]}
-              style={Object {}}
+            <div
+              className="ant-form-item-control "
             >
-              Post
-            </button>
+              <button
+                className="Action ant-btn-lg Action--primary"
+                disabled={false}
+                onClick={[Function]}
+                style={Object {}}
+              >
+                Post
+              </button>
+            </div>
           </div>
         </div>
       </div>