Merge pull request #10102 from BerkeleyTrue/feature/add-redux-night-mode

Feature(theme): add nightmode react logic
pull/10111/head
Mrugesh Mohapatra 2016-08-06 22:51:01 +05:30 committed by GitHub
commit 8579c16ac6
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 gitterSaga from './gitter-saga';
import mouseTrapSaga from './mouse-trap-saga'; import mouseTrapSaga from './mouse-trap-saga';
import analyticsSaga from './analytics-saga'; import analyticsSaga from './analytics-saga';
import nightModeSaga from './night-mode-saga';
export default [ export default [
errSaga, errSaga,
@ -19,5 +20,6 @@ export default [
codeStorageSaga, codeStorageSaga,
gitterSaga, gitterSaga,
mouseTrapSaga, 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, updateThisUser,
updateCompletedChallenges, updateCompletedChallenges,
createErrorObservable, createErrorObservable,
showSignIn showSignIn,
updateTheme,
addThemeToBody
} from './actions'; } from './actions';
const { fetchUser } = types; const { fetchUser } = types;
export default function getUserSaga(action$, getState, { services }) { export default function getUserSaga(action$, getState, { services }) {
return action$ return action$
.filter(action => action.type === fetchUser) .filter(action => action.type === fetchUser)
@ -19,10 +20,14 @@ export default function getUserSaga(action$, getState, { services }) {
if (!entities || !result) { if (!entities || !result) {
return Observable.just(showSignIn()); return Observable.just(showSignIn());
} }
const user = entities.user[result];
const isNightMode = user.theme === 'night';
return Observable.of( return Observable.of(
addUser(entities), addUser(entities),
updateCompletedChallenges(result),
updateThisUser(result), updateThisUser(result),
updateCompletedChallenges(result) isNightMode ? updateTheme(user.theme) : null,
isNightMode ? addThemeToBody(user.theme) : null
); );
}) })
.catch(createErrorObservable); .catch(createErrorObservable);

View File

@ -10,7 +10,8 @@ const initialState = {
windowHeight: 0, windowHeight: 0,
navHeight: 0, navHeight: 0,
isMainChatOpen: false, isMainChatOpen: false,
isHelpChatOpen: false isHelpChatOpen: false,
theme: 'default'
}; };
export default handleActions( export default handleActions(
@ -29,6 +30,10 @@ export default handleActions(
...state, ...state,
lang: payload lang: payload
}), }),
[types.updateTheme]: (state, { payload = 'default' }) => ({
...state,
theme: payload
}),
[types.showSignIn]: state => ({ [types.showSignIn]: state => ({
...state, ...state,
shouldShowSignIn: true shouldShowSignIn: true

View File

@ -18,7 +18,6 @@ export default createTypes([
'updateMyCurrentChallenge', 'updateMyCurrentChallenge',
'handleError', 'handleError',
'toggleNightMode',
// used to hit the server // used to hit the server
'hardGoTo', 'hardGoTo',
'delayedRedirect', 'delayedRedirect',
@ -44,5 +43,10 @@ export default createTypes([
'openHelpChat', 'openHelpChat',
'closeHelpChat', 'closeHelpChat',
'toggleHelpChat' 'toggleHelpChat',
// night mode
'toggleNightMode',
'updateTheme',
'addThemeToBody'
], 'app'); ], 'app');

View File

@ -599,6 +599,7 @@ module.exports = function(User) {
.toPromise(); .toPromise();
}; };
// deprecated. remove once live
User.remoteMethod( User.remoteMethod(
'updateTheme', '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( api.post(
'/toggle-lockdown', '/toggle-lockdown',
ifNoUser401,
toggleUserFlag('isLocked') toggleUserFlag('isLocked')
); );
api.post( api.post(
@ -99,5 +117,12 @@ export default function settingsController(app) {
ifNoUser401, ifNoUser401,
updateMyCurrentChallenge updateMyCurrentChallenge
); );
api.post(
'/update-my-theme',
ifNoUser401,
updateMyTheme
);
app.use(api); app.use(api);
} }