Commit a5e336a1 authored by Ray Schamp's avatar Ray Schamp

Use redux for session handling

parent 1205ed01
......@@ -22,5 +22,8 @@ module.exports = {
sentry_dsn: process.env.CLIENT_SENTRY_DSN || '',
// Use minified JS libraries
min: (process.env.NODE_ENV === 'production') ? '.min' : ''
min: (process.env.NODE_ENV === 'production') ? '.min' : '',
// Redux likes to have this
NODE_ENV: process.env.NODE_ENV
};
......@@ -46,6 +46,7 @@
<!-- Scripts -->
<script src="/js/lib/react{{min}}.js"></script>
<script src="/js/lib/react-dom{{min}}.js"></script>
<script src="/js/lib/redux{{min}}.js"></script>
<script src="/js/lib/react-intl-with-locales{{min}}.js"></script>
<script src="/js/lib/raven.min.js"></script>
......
var React = require('react');
var connect = require('react-redux').connect;
var Button = require('../../components/forms/button.jsx');
var Session = require('../../mixins/session.jsx');
require('./adminpanel.scss');
var AdminPanel = React.createClass({
type: 'AdminPanel',
mixins: [
Session
],
getInitialState: function () {
return {
showPanel: false
......@@ -22,8 +19,8 @@ var AdminPanel = React.createClass({
render: function () {
// make sure user is present before checking if they're an admin. Don't show anything if user not an admin.
var showAdmin = false;
if (this.state.session.user) {
showAdmin = this.state.session.permissions.admin;
if (this.props.session.user) {
showAdmin = this.props.session.permissions.admin;
}
if (!showAdmin) return false;
......@@ -78,4 +75,12 @@ var AdminPanel = React.createClass({
}
});
module.exports = AdminPanel;
var mapStateToProps = function (state) {
return {
session: state.session
};
};
var ConnectedAdminPanel = connect(mapStateToProps)(AdminPanel);
module.exports = ConnectedAdminPanel;
var connect = require('react-redux').connect;
var omit = require('lodash.omit');
var React = require('react');
var actions = require('../../redux/actions.js');
var Modal = require('../modal/modal.jsx');
var Registration = require('../registration/registration.jsx');
......@@ -15,7 +18,6 @@ var Intro = React.createClass({
},
getDefaultProps: function () {
return {
projectCount: 10569070,
messages: {
'intro.aboutScratch': 'ABOUT SCRATCH',
'intro.forEducators': 'FOR EDUCATORS',
......@@ -24,7 +26,9 @@ var Intro = React.createClass({
'intro.seeExamples': 'SEE EXAMPLES',
'intro.tagLine': 'Create stories, games, and animations<br /> Share with others around the world',
'intro.tryItOut': 'TRY IT OUT'
}
},
projectCount: 10569070,
session: {}
};
},
getInitialState: function () {
......@@ -46,7 +50,7 @@ var Intro = React.createClass({
this.setState({'registrationOpen': false});
},
completeRegistration: function () {
window.refreshSession();
this.props.dispatch(actions.refreshSession());
this.closeRegistration();
},
render: function () {
......@@ -145,4 +149,12 @@ var Intro = React.createClass({
}
});
module.exports = Intro;
var mapStateToProps = function (state) {
return {
session: state.session
};
};
var ConnectedIntro = connect(mapStateToProps)(Intro);
module.exports = ConnectedIntro;
var classNames = require('classnames');
var connect = require('react-redux').connect;
var React = require('react');
var ReactIntl = require('react-intl');
var defineMessages = ReactIntl.defineMessages;
var FormattedMessage = ReactIntl.FormattedMessage;
var injectIntl = ReactIntl.injectIntl;
var actions = require('../../redux/actions.js');
var Api = require('../../mixins/api.jsx');
var Avatar = require('../avatar/avatar.jsx');
var Dropdown = require('./dropdown.jsx');
......@@ -13,7 +16,6 @@ var log = require('../../lib/log.js');
var Login = require('../login/login.jsx');
var Modal = require('../modal/modal.jsx');
var Registration = require('../registration/registration.jsx');
var Session = require('../../mixins/session.jsx');
require('./navigation.scss');
......@@ -37,8 +39,7 @@ var defaultMessages = defineMessages({
var Navigation = React.createClass({
type: 'Navigation',
mixins: [
Api,
Session
Api
],
getInitialState: function () {
return {
......@@ -51,20 +52,25 @@ var Navigation = React.createClass({
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
};
},
getDefaultProps: function () {
return {
session: {}
};
},
componentDidMount: function () {
if (this.state.session.user) {
if (this.props.session.user) {
this.getMessageCount();
var intervalId = setInterval(this.getMessageCount, 120000); // check for new messages every 2 mins.
this.setState({'messageCountIntervalId': intervalId});
}
},
componentDidUpdate: function (prevProps, prevState) {
if (prevState.session.user != this.state.session.user) {
if (prevProps.session.user != this.props.session.user) {
this.setState({
'loginOpen': false,
'accountNavOpen': false
});
if (this.state.session.user) {
if (this.props.session.user) {
this.getMessageCount();
var intervalId = setInterval(this.getMessageCount, 120000);
this.setState({'messageCountIntervalId': intervalId});
......@@ -89,13 +95,13 @@ var Navigation = React.createClass({
}
},
getProfileUrl: function () {
if (!this.state.session.user) return;
return '/users/' + this.state.session.user.username + '/';
if (!this.props.session.user) return;
return '/users/' + this.props.session.user.username + '/';
},
getMessageCount: function () {
this.api({
method: 'get',
uri: '/users/' + this.state.session.user.username + '/messages/count'
uri: '/users/' + this.props.session.user.username + '/messages/count'
}, function (err, body) {
if (err) return this.setState({'unreadMessageCount': 0});
if (body) {
......@@ -141,7 +147,7 @@ var Navigation = React.createClass({
this.showCanceledDeletion();
}
}.bind(this));
window.refreshSession();
this.props.dispatch(actions.refreshSession());
}
}
// JS error already logged by api mixin
......@@ -158,7 +164,7 @@ var Navigation = React.createClass({
}, function (err) {
if (err) log.error(err);
this.closeLogin();
window.refreshSession();
this.props.dispatch(actions.refreshSession());
}.bind(this));
},
handleAccountNavClick: function (e) {
......@@ -178,20 +184,20 @@ var Navigation = React.createClass({
this.setState({'registrationOpen': false});
},
completeRegistration: function () {
window.refreshSession();
this.props.dispatch(actions.refreshSession());
this.closeRegistration();
},
render: function () {
var classes = classNames({
'inner': true,
'logged-in': this.state.session.user
'logged-in': this.props.session.user
});
var messageClasses = classNames({
'messageCount': true,
'show': this.state.unreadMessageCount > 0
});
var formatMessage = this.props.intl.formatMessage;
var createLink = this.state.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
var createLink = this.props.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
return (
<div className={classes}>
<ul>
......@@ -244,7 +250,7 @@ var Navigation = React.createClass({
<Input type="hidden" name="sort_by" value="datetime_shared" />
</form>
</li>
{this.state.session.user ? [
{this.props.session.user ? [
<li className="link right messages" key="messages">
<a
href="/messages/"
......@@ -264,8 +270,8 @@ var Navigation = React.createClass({
</li>,
<li className="link right account-nav" key="account-nav">
<a className="userInfo" href="#" onClick={this.handleAccountNavClick}>
<Avatar src={this.state.session.user.thumbnailUrl} alt="" />
{this.state.session.user.username}
<Avatar src={this.props.session.user.thumbnailUrl} alt="" />
{this.props.session.user.username}
</a>
<Dropdown
as="ul"
......@@ -283,7 +289,7 @@ var Navigation = React.createClass({
<FormattedMessage {...defaultMessages.myStuff} />
</a>
</li>
{this.state.session.permissions.educator ? [
{this.props.session.permissions.educator ? [
<li>
<a href="/educators/classes/">
<FormattedMessage
......@@ -357,4 +363,12 @@ var Navigation = React.createClass({
}
});
module.exports = injectIntl(Navigation);
var mapStateToProps = function (state) {
return {
session: state.session
};
};
var ConnectedNavigation = connect(mapStateToProps)(Navigation);
module.exports = injectIntl(ConnectedNavigation);
var api = require('./mixins/api.jsx').api;
var jar = require('./lib/jar');
/**
* -----------------------------------------------------------------------------
* Session
* -----------------------------------------------------------------------------
*/
(function () {
window._session = {};
/**
* Binds the object to private session variable and dispatches a global
* "session" event.
*
* @param {object} Session object
*
* @return {void}
*/
window.updateSession = function (session) {
window._session = session;
var sessionEvent = new CustomEvent('session', session);
window.dispatchEvent(sessionEvent);
};
/**
* Gets a session object from the local proxy method. Calls "updateSession"
* once session has been returned from the proxy.
*
* @return {void}
*/
window.refreshSession = function () {
api({
host: '',
uri: '/session/'
}, function (err, body) {
if (err) return;
if (typeof body !== 'undefined') {
if (body.banned) {
return window.location = body.redirectUrl;
} else {
window.updateSession(body);
}
}
});
};
// Fetch session
window.refreshSession();
})();
/**
* -----------------------------------------------------------------------------
......
var redux = require('redux');
var thunk = require('redux-thunk').default;
var ReactDOM = require('react-dom');
var StoreProvider = require('react-redux').Provider;
var IntlProvider = require('./intl.jsx').IntlProvider;
var actions = require('../redux/actions.js');
var reducer = require('../redux/reducer.js');
require('../main.scss');
var store = redux.createStore(
reducer,
redux.applyMiddleware(thunk)
);
var render = function (jsx, element) {
// Get locale and messages from global namespace (see "init.js")
......@@ -20,12 +29,16 @@ var render = function (jsx, element) {
// Render view component
ReactDOM.render(
<IntlProvider locale={locale} messages={messages}>
{jsx}
</IntlProvider>,
<StoreProvider store={store}>
<IntlProvider locale={locale} messages={messages}>
{jsx}
</IntlProvider>
</StoreProvider>,
element
);
// Get initial session
store.dispatch(actions.refreshSession());
};
module.exports = render;
var Session = {
getInitialState: function () {
return {
session: window._session
};
},
updateSession: function () {
this.setState({'session': window._session});
},
componentWillMount: function () {
window.addEventListener('session', this.updateSession);
},
componentWillUnmount: function () {
window.removeEventListener('session', this.updateSession);
}
};
module.exports = Session;
var keyMirror = require('keymirror');
var api = require('../mixins/api.jsx').api;
var Types = keyMirror({
REFRESH_SESSION: null,
SET_SESSION: null,
SET_SESSION_ERROR: null
});
var Actions = {
types: Types,
setSessionError: function (error) {
return {
type: Types.SET_SESSION_ERROR,
error: error
}
},
setSession: function (session) {
return {
type: Types.SET_SESSION,
session: session
}
},
refreshSession: function () {
return function (dispatch) {
api({
host: '',
uri: '/session/'
}, function (err, body) {
if (err) return dispatch(Actions.setSessionError(err));
if (typeof body !== 'undefined') {
if (body.banned) {
return window.location = url;
} else {
return dispatch(Actions.setSession(body));
}
}
});
};
},
};
module.exports = Actions;
var combineReducers = require('redux').combineReducers;
var actionTypes = require('./actions.js').types;
var sessionReducer = function (state, action) {
// Reducer for handling changes to session state
if (typeof state === 'undefined') {
state = {};
}
switch (action.type) {
case actionTypes.SET_SESSION:
return action.session;
case actionTypes.SET_SESSION_ERROR:
// TODO: do something with action.error
return state;
default:
return state;
}
};
var appReducer = combineReducers({
session: sessionReducer
});
module.exports = appReducer;
var connect = require('react-redux').connect;
var injectIntl = require('react-intl').injectIntl;
var omit = require('lodash.omit');
var React = require('react');
var render = require('../../lib/render.jsx');
var actions = require('../../redux/actions.js');
var Api = require('../../mixins/api.jsx');
var Session = require('../../mixins/session.jsx');
var Activity = require('../../components/activity/activity.jsx');
var AdminPanel = require('../../components/adminpanel/adminpanel.jsx');
......@@ -23,8 +25,7 @@ require('./splash.scss');
var Splash = injectIntl(React.createClass({
type: 'Splash',
mixins: [
Api,
Session
Api
],
getInitialState: function () {
return {
......@@ -37,9 +38,14 @@ var Splash = injectIntl(React.createClass({
refreshCacheStatus: 'notrequested'
};
},
getDefaultProps: function () {
return {
session: {}
}
},
componentDidUpdate: function (prevProps, prevState) {
if (this.state.session.user != prevState.session.user) {
if (this.state.session.user) {
if (this.props.session.user != prevProps.session.user) {
if (this.props.session.user) {
this.getActivity();
this.getFeaturedCustom();
this.getNews();
......@@ -58,7 +64,7 @@ var Splash = injectIntl(React.createClass({
},
componentDidMount: function () {
this.getFeaturedGlobal();
if (this.state.session.user) {
if (this.props.session.user) {
this.getActivity();
this.getFeaturedCustom();
this.getNews();
......@@ -84,7 +90,7 @@ var Splash = injectIntl(React.createClass({
},
getActivity: function () {
this.api({
uri: '/proxy/users/' + this.state.session.user.username + '/activity?limit=5'
uri: '/proxy/users/' + this.props.session.user.username + '/activity?limit=5'
}, function (err, body) {
if (!err) this.setState({activity: body});
}.bind(this));
......@@ -98,7 +104,7 @@ var Splash = injectIntl(React.createClass({
},
getFeaturedCustom: function () {
this.api({
uri: '/proxy/users/' + this.state.session.user.id + '/featured'
uri: '/proxy/users/' + this.props.session.user.id + '/featured'
}, function (err, body) {
if (!err) this.setState({featuredCustom: body});
}.bind(this));
......@@ -161,20 +167,20 @@ var Splash = injectIntl(React.createClass({
useCsrf: true,
json: {cue: cue, value: false}
}, function (err) {
if (!err) window.refreshSession();
if (!err) this.props.dispatch(actions.refreshSession());
});
},
shouldShowWelcome: function () {
if (!this.state.session.user || !this.state.session.flags.show_welcome) return false;
if (!this.props.session.user || !this.props.session.flags.show_welcome) return false;
return (
new Date(this.state.session.user.dateJoined) >
new Date(this.props.session.user.dateJoined) >
new Date(new Date - 2*7*24*60*60*1000) // Two weeks ago
);
},
shouldShowEmailConfirmation: function () {
return (
this.state.session.user && this.state.session.flags.has_outstanding_email_confirmation &&
this.state.session.flags.confirm_email_banner);
this.props.session.user && this.props.session.flags.has_outstanding_email_confirmation &&
this.props.session.flags.confirm_email_banner);
},
renderHomepageRows: function () {
var formatMessage = this.props.intl.formatMessage;
......@@ -233,7 +239,7 @@ var Splash = injectIntl(React.createClass({
);
}
if (this.state.session.user &&
if (this.props.session.user &&
this.state.featuredGlobal.community_newest_projects &&
this.state.featuredGlobal.community_newest_projects.length > 0) {
......@@ -358,7 +364,7 @@ var Splash = injectIntl(React.createClass({
</Modal>
] : []}
<div key="inner" className="inner">
{this.state.session.user ? [
{this.props.session.user ? [
<div key="header" className="splash-header">
{this.shouldShowWelcome() ? [
<Welcome key="welcome"
......@@ -412,4 +418,12 @@ var Splash = injectIntl(React.createClass({
}
}));
render(<Page><Splash /></Page>, document.getElementById('app'));
var mapStateToProps = function (state) {
return {
session: state.session
};
};
var ConnectedSplash = connect(mapStateToProps)(Splash);
render(<Page><ConnectedSplash /></Page>, document.getElementById('app'));
This diff is collapsed.
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Redux=e():t.Redux=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0,e.compose=e.applyMiddleware=e.bindActionCreators=e.combineReducers=e.createStore=void 0;var o=n(2),i=r(o),u=n(7),c=r(u),a=n(6),f=r(a),s=n(5),d=r(s),l=n(1),p=r(l),y=n(3);r(y);e.createStore=i["default"],e.combineReducers=c["default"],e.bindActionCreators=f["default"],e.applyMiddleware=d["default"],e.compose=p["default"]},function(t,e){"use strict";function n(){for(var t=arguments.length,e=Array(t),n=0;t>n;n++)e[n]=arguments[n];return function(){if(0===e.length)return arguments.length>0?arguments[0]:void 0;var t=e[e.length-1],n=e.slice(0,-1);return n.reduceRight(function(t,e){return e(t)},t.apply(void 0,arguments))}}e.__esModule=!0,e["default"]=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e,n){function r(){y===p&&(y=p.slice())}function i(){return l}function a(t){if("function"!=typeof t)throw Error("Expected listener to be a function.");var e=!0;return r(),y.push(t),function(){if(e){e=!1,r();var n=y.indexOf(t);y.splice(n,1)}}}function f(t){if(!(0,u["default"])(t))throw Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===t.type)throw Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(h)throw Error("Reducers may not dispatch actions.");try{h=!0,l=d(l,t)}finally{h=!1}for(var e=p=y,n=0;e.length>n;n++)e[n]();return t}function s(t){if("function"!=typeof t)throw Error("Expected the nextReducer to be a function.");d=t,f({type:c.INIT})}if("function"==typeof e&&void 0===n&&(n=e,e=void 0),void 0!==n){if("function"!=typeof n)throw Error("Expected the enhancer to be a function.");return n(o)(t,e)}if("function"!=typeof t)throw Error("Expected the reducer to be a function.");var d=t,l=e,p=[],y=p,h=!1;return f({type:c.INIT}),{dispatch:f,subscribe:a,getState:i,replaceReducer:s}}e.__esModule=!0,e.ActionTypes=void 0,e["default"]=o;var i=n(4),u=r(i),c=e.ActionTypes={INIT:"@@redux/INIT"}},function(t,e){"use strict";function n(t){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(t);try{throw Error(t)}catch(e){}}e.__esModule=!0,e["default"]=n},function(t,e,n){function r(t){if(!i(t)||s.call(t)!=u||o(t))return!1;var e=c;if("function"==typeof t.constructor&&(e=d(t)),null===e)return!0;var n=e.constructor;return"function"==typeof n&&n instanceof n&&a.call(n)==f}var o=n(8),i=n(9),u="[object Object]",c=Object.prototype,a=Function.prototype.toString,f=a.call(Object),s=c.toString,d=Object.getPrototypeOf;t.exports=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(){for(var t=arguments.length,e=Array(t),n=0;t>n;n++)e[n]=arguments[n];return function(t){return function(n,r,o){var u=t(n,r,o),a=u.dispatch,f=[],s={getState:u.getState,dispatch:function(t){return a(t)}};return f=e.map(function(t){return t(s)}),a=c["default"].apply(void 0,f)(u.dispatch),i({},u,{dispatch:a})}}}var i=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t};e.__esModule=!0,e["default"]=o;var u=n(1),c=r(u)},function(t,e){"use strict";function n(t,e){return function(){return e(t.apply(void 0,arguments))}}function r(t,e){if("function"==typeof t)return n(t,e);if("object"!=typeof t||null===t)throw Error("bindActionCreators expected an object or a function, instead received "+(null===t?"null":typeof t)+'. Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');for(var r=Object.keys(t),o={},i=0;r.length>i;i++){var u=r[i],c=t[u];"function"==typeof c&&(o[u]=n(c,e))}return o}e.__esModule=!0,e["default"]=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){var n=e&&e.type,r=n&&'"'+n+'"'||"an action";return'Reducer "'+t+'" returned undefined handling '+r+". To ignore an action, you must explicitly return the previous state."}function i(t){Object.keys(t).forEach(function(e){var n=t[e],r=n(void 0,{type:c.ActionTypes.INIT});if(void 0===r)throw Error('Reducer "'+e+'" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined.');var o="@@redux/PROBE_UNKNOWN_ACTION_"+Math.random().toString(36).substring(7).split("").join(".");if(void 0===n(void 0,{type:o}))throw Error('Reducer "'+e+'" returned undefined when probed with a random type. '+("Don't try to handle "+c.ActionTypes.INIT+' or other actions in "redux/*" ')+"namespace. They are considered private. Instead, you must return the current state for any unknown actions, unless it is undefined, in which case you must return the initial state, regardless of the action type. The initial state may not be undefined.")})}function u(t){for(var e=Object.keys(t),n={},r=0;e.length>r;r++){var u=e[r];"function"==typeof t[u]&&(n[u]=t[u])}var c,a=Object.keys(n);try{i(n)}catch(f){c=f}return function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=arguments[1];if(c)throw c;for(var r=!1,i={},u=0;a.length>u;u++){var f=a[u],s=n[f],d=t[f],l=s(d,e);if(void 0===l){var p=o(f,e);throw Error(p)}i[f]=l,r=r||l!==d}return r?i:t}}e.__esModule=!0,e["default"]=u;var c=n(2),a=n(4),f=(r(a),n(3));r(f)},function(t,e){function n(t){var e=!1;if(null!=t&&"function"!=typeof t.toString)try{e=!!(t+"")}catch(n){}return e}t.exports=n},function(t,e){function n(t){return!!t&&"object"==typeof t}t.exports=n}])});
\ No newline at end of file
......@@ -20,7 +20,8 @@ module.exports = {
'react': 'React',
'react/addons': 'React',
'react-dom': 'ReactDOM',
'react-intl': 'ReactIntl'
'react-intl': 'ReactIntl',
'redux': 'Redux'
},
output: {
path: path.resolve(__dirname, 'build'),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment