From 94c4c846e9aa5898059c464b227ff2b0ba5731f3 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 5 Aug 2016 14:05:57 -0700 Subject: [PATCH] Feature(theme): add nightmode react logic We wait to load the user before applying the theme as we will begin aggressively caching most of the react app routes. This means we can not depend on user data to determine. --- client/sagas/index.js | 4 ++- client/sagas/night-mode-saga.js | 47 +++++++++++++++++++++++++++++ common/app/redux/actions.js | 11 ++++++- common/app/redux/fetch-user-saga.js | 11 +++++-- common/app/redux/reducer.js | 7 ++++- common/app/redux/types.js | 8 +++-- common/models/user.js | 1 + server/boot/settings.js | 25 +++++++++++++++ 8 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 client/sagas/night-mode-saga.js diff --git a/client/sagas/index.js b/client/sagas/index.js index ec4074af584..13e755f9fad 100644 --- a/client/sagas/index.js +++ b/client/sagas/index.js @@ -8,6 +8,7 @@ import codeStorageSaga from './code-storage-saga'; import gitterSaga from './gitter-saga'; import mouseTrapSaga from './mouse-trap-saga'; import analyticsSaga from './analytics-saga'; +import nightModeSaga from './night-mode-saga'; export default [ errSaga, @@ -19,5 +20,6 @@ export default [ codeStorageSaga, gitterSaga, mouseTrapSaga, - analyticsSaga + analyticsSaga, + nightModeSaga ]; diff --git a/client/sagas/night-mode-saga.js b/client/sagas/night-mode-saga.js new file mode 100644 index 00000000000..efc5e9d5c93 --- /dev/null +++ b/client/sagas/night-mode-saga.js @@ -0,0 +1,47 @@ +import { Observable } from 'rx'; +import { postJSON$ } from '../../common/utils/ajax-stream'; +import types from '../../common/app/redux/types'; +import { + addThemeToBody, + updateTheme, + createErrorObservable +} from '../../common/app/redux/actions'; + +export default function nightModeSaga( + actions, + getState, + { document: { body } } +) { + const toggleBodyClass = actions + .filter(({ type }) => types.addThemeToBody === type) + .doOnNext(({ payload: theme }) => { + if (theme === 'night') { + body.classList.add('night'); + } else { + body.classList.remove('night'); + } + }) + .filter(() => false); + const toggle = actions + .filter(({ type }) => types.toggleNightMode === type); + + const optimistic = toggle + .flatMap(() => { + const { app: { theme } } = getState(); + const newTheme = !theme || theme === 'default' ? 'night' : 'default'; + return Observable.of( + updateTheme(newTheme), + addThemeToBody(newTheme) + ); + }); + + const ajax = toggle + .debounce(250) + .flatMapLatest(() => { + const { app: { theme, csrfToken: _csrf } } = getState(); + return postJSON$('/update-my-theme', { _csrf, theme }) + .catch(createErrorObservable); + }); + + return Observable.merge(optimistic, toggleBodyClass, ajax); +} diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js index 71d6c50e02c..83fefaf255b 100644 --- a/common/app/redux/actions.js +++ b/common/app/redux/actions.js @@ -188,4 +188,13 @@ export const closeHelpChat = createAction( }) ); -export const toggleNightMode = createAction(types.toggleNightMode); +export const toggleNightMode = createAction( + types.toggleNightMode, + // we use this function to avoid hanging onto the eventObject + // so that react can recycle it + () => null +); +// updateTheme(theme: /night|default/) => Action +export const updateTheme = createAction(types.updateTheme); +// addThemeToBody(theme: /night|default/) => Action +export const addThemeToBody = createAction(types.addThemeToBody); diff --git a/common/app/redux/fetch-user-saga.js b/common/app/redux/fetch-user-saga.js index ba2f6cbc4ee..5afa9b58722 100644 --- a/common/app/redux/fetch-user-saga.js +++ b/common/app/redux/fetch-user-saga.js @@ -5,11 +5,12 @@ import { updateThisUser, updateCompletedChallenges, createErrorObservable, - showSignIn + showSignIn, + updateTheme, + addThemeToBody } from './actions'; const { fetchUser } = types; - export default function getUserSaga(action$, getState, { services }) { return action$ .filter(action => action.type === fetchUser) @@ -19,10 +20,14 @@ export default function getUserSaga(action$, getState, { services }) { if (!entities || !result) { return Observable.just(showSignIn()); } + const user = entities.user[result]; + const isNightMode = user.theme === 'night'; return Observable.of( addUser(entities), + updateCompletedChallenges(result), updateThisUser(result), - updateCompletedChallenges(result) + isNightMode ? updateTheme(user.theme) : null, + isNightMode ? addThemeToBody(user.theme) : null ); }) .catch(createErrorObservable); diff --git a/common/app/redux/reducer.js b/common/app/redux/reducer.js index b73b20cccfb..4ccf742824d 100644 --- a/common/app/redux/reducer.js +++ b/common/app/redux/reducer.js @@ -10,7 +10,8 @@ const initialState = { windowHeight: 0, navHeight: 0, isMainChatOpen: false, - isHelpChatOpen: false + isHelpChatOpen: false, + theme: 'default' }; export default handleActions( @@ -29,6 +30,10 @@ export default handleActions( ...state, lang: payload }), + [types.updateTheme]: (state, { payload = 'default' }) => ({ + ...state, + theme: payload + }), [types.showSignIn]: state => ({ ...state, shouldShowSignIn: true diff --git a/common/app/redux/types.js b/common/app/redux/types.js index d0ec7220cb0..1da89d89c35 100644 --- a/common/app/redux/types.js +++ b/common/app/redux/types.js @@ -18,7 +18,6 @@ export default createTypes([ 'updateMyCurrentChallenge', 'handleError', - 'toggleNightMode', // used to hit the server 'hardGoTo', 'delayedRedirect', @@ -44,5 +43,10 @@ export default createTypes([ 'openHelpChat', 'closeHelpChat', - 'toggleHelpChat' + 'toggleHelpChat', + + // night mode + 'toggleNightMode', + 'updateTheme', + 'addThemeToBody' ], 'app'); diff --git a/common/models/user.js b/common/models/user.js index ee3bfb52851..31344790762 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -599,6 +599,7 @@ module.exports = function(User) { .toPromise(); }; + // deprecated. remove once live User.remoteMethod( 'updateTheme', { diff --git a/server/boot/settings.js b/server/boot/settings.js index 496a2df2496..9cc0900407f 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -64,8 +64,26 @@ export default function settingsController(app) { ); } + function updateMyTheme(req, res, next) { + req.checkBody('theme', 'Theme is invalid.').isLength({ min: 4 }); + const { body: { theme } } = req; + const errors = req.validationErrors(true); + if (errors) { + return res.status(403).json({ errors }); + } + if (req.user.theme === theme) { + return res.json({ msg: 'Theme already set' }); + } + return req.user.updateTheme('' + theme) + .then( + data => res.json(data), + next + ); + } + api.post( '/toggle-lockdown', + ifNoUser401, toggleUserFlag('isLocked') ); api.post( @@ -99,5 +117,12 @@ export default function settingsController(app) { ifNoUser401, updateMyCurrentChallenge ); + + api.post( + '/update-my-theme', + ifNoUser401, + updateMyTheme + ); + app.use(api); }