Merge pull request #10102 from BerkeleyTrue/feature/add-redux-night-mode
Feature(theme): add nightmode react logicpull/10111/head
commit
8579c16ac6
|
@ -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
|
||||||
];
|
];
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -599,6 +599,7 @@ module.exports = function(User) {
|
||||||
.toPromise();
|
.toPromise();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// deprecated. remove once live
|
||||||
User.remoteMethod(
|
User.remoteMethod(
|
||||||
'updateTheme',
|
'updateTheme',
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue