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.
This commit is contained in:
Berkeley Martinez
2016-08-05 14:05:57 -07:00
parent f326acb47c
commit 94c4c846e9
8 changed files with 106 additions and 8 deletions

View File

@ -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
];

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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');

View File

@ -599,6 +599,7 @@ module.exports = function(User) {
.toPromise();
};
// deprecated. remove once live
User.remoteMethod(
'updateTheme',
{

View File

@ -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);
}