diff --git a/src/components/comment/comment.jsx b/src/components/comment/comment.jsx
new file mode 100644
index 000000000..4a324799a
--- /dev/null
+++ b/src/components/comment/comment.jsx
@@ -0,0 +1,34 @@
+var classNames = require('classnames');
+var FormattedRelative = require('react-intl').FormattedRelative;
+var React = require('react');
+
+var EmojiText = require('../emoji-text/emoji-text.jsx');
+
+require('./comment.scss');
+
+var CommentText = React.createClass({
+ type: 'CommentText',
+ propTypes: {
+ comment: React.PropTypes.string.isRequired,
+ datetimeCreated: React.PropTypes.string,
+ className: React.PropTypes.string
+ },
+ render: function () {
+ var classes = classNames(
+ 'comment-text',
+ this.props.class
+ );
+ return (
+
+
+ {typeof this.props.datetimeCreated !== 'undefined' ? [
+
+
+
+ ] : []}
+
+ );
+ }
+});
+
+module.exports = CommentText;
diff --git a/src/components/comment/comment.scss b/src/components/comment/comment.scss
new file mode 100644
index 000000000..e772a849c
--- /dev/null
+++ b/src/components/comment/comment.scss
@@ -0,0 +1,42 @@
+@import "../../colors";
+
+.comment-text {
+ position: relative;
+ border: 1px solid $ui-border;
+ border-radius: 0 5px 5px 5px;
+ padding: 1rem;
+}
+
+.comment-text:before {
+ display: block;
+ position: absolute;
+ top: -1px;
+ left: -13px;
+ border-top: 12px solid $ui-border;
+ border-left: 13px solid $ui-border;
+ border-radius: 0 0 0 13px;
+ width: 0;
+ content: "";
+}
+
+.comment-text:after {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: -12px;
+ border-top: 10px solid $ui-white;
+ border-left: 12px solid $ui-white;
+ border-radius: 0 0 0 12px;
+ width: 0;
+ content: "";
+}
+
+.emoji-text.mod-comment {
+ margin: 0;
+}
+
+.comment-text-timestamp {
+ margin: 1rem 0 0;
+ color: $ui-dark-gray;
+ font-size: .8rem;
+}
diff --git a/src/components/emoji-text/emoji-text.jsx b/src/components/emoji-text/emoji-text.jsx
new file mode 100644
index 000000000..c6f382260
--- /dev/null
+++ b/src/components/emoji-text/emoji-text.jsx
@@ -0,0 +1,33 @@
+var classNames = require('classnames');
+var React = require('react');
+
+require('./emoji-text.scss');
+
+var EmojiText = React.createClass({
+ type: 'EmojiText',
+ propTyes: {
+ text: React.PropTypes.string.isRequired,
+ className: React.PropTypes.string
+ },
+ getDefaultProps: function () {
+ return {
+ as: 'p'
+ };
+ },
+ render: function () {
+ var classes = classNames(
+ 'emoji-text',
+ this.props.className
+ );
+ return (
+
+ );
+ }
+});
+
+module.exports = EmojiText;
diff --git a/src/components/emoji-text/emoji-text.scss b/src/components/emoji-text/emoji-text.scss
new file mode 100644
index 000000000..a5f1e0ee2
--- /dev/null
+++ b/src/components/emoji-text/emoji-text.scss
@@ -0,0 +1,4 @@
+.emoji {
+ max-width: 24px;
+ vertical-align: middle;
+}
diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx
index b7ab808f0..6c65bf792 100644
--- a/src/components/navigation/www/navigation.jsx
+++ b/src/components/navigation/www/navigation.jsx
@@ -5,6 +5,7 @@ var ReactIntl = require('react-intl');
var FormattedMessage = ReactIntl.FormattedMessage;
var injectIntl = ReactIntl.injectIntl;
+var messageCountActions = require('../../../redux/message-count.js');
var sessionActions = require('../../../redux/session.js');
var api = require('../../../lib/api');
@@ -30,20 +31,21 @@ var Navigation = React.createClass({
loginOpen: false,
loginError: null,
registrationOpen: false,
- unreadMessageCount: 0, // bubble number to display how many notifications someone has.
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
};
},
getDefaultProps: function () {
return {
session: {},
+ unreadMessageCount: 0, // bubble number to display how many notifications someone has.
searchTerm: ''
};
},
componentDidMount: function () {
if (this.props.session.session.user) {
- this.getMessageCount();
- var intervalId = setInterval(this.getMessageCount, 120000); // check for new messages every 2 mins.
+ var intervalId = setInterval(
+ this.props.dispatch(messageCountActions.getCount(this.props.session.session.user.username), 120000)
+ ); // check for new messages every 2 mins.
this.setState({'messageCountIntervalId': intervalId});
}
},
@@ -54,14 +56,15 @@ var Navigation = React.createClass({
'accountNavOpen': false
});
if (this.props.session.session.user) {
- this.getMessageCount();
- var intervalId = setInterval(this.getMessageCount, 120000);
+ var intervalId = setInterval(
+ this.props.dispatch(messageCountActions.getCount(this.props.session.session.user.username), 120000)
+ ); // check for new messages every 2 mins.
this.setState({'messageCountIntervalId': intervalId});
} else {
// clear message count check, and set to default id.
clearInterval(this.state.messageCountIntervalId);
+ this.props.dispatch(messageCountActions.setCount(0));
this.setState({
- 'unreadMessageCount': 0,
'messageCountIntervalId': -1
});
}
@@ -71,8 +74,8 @@ var Navigation = React.createClass({
// clear message interval if it exists
if (this.state.messageCountIntervalId != -1) {
clearInterval(this.state.messageCountIntervalId);
+ this.props.dispatch(messageCountActions.setCount(0));
this.setState({
- 'unreadMessageCount': 0,
'messageCountIntervalId': -1
});
}
@@ -81,18 +84,6 @@ var Navigation = React.createClass({
if (!this.props.session.session.user) return;
return '/users/' + this.props.session.session.user.username + '/';
},
- getMessageCount: function () {
- api({
- method: 'get',
- uri: '/users/' + this.props.session.session.user.username + '/messages/count'
- }, function (err, body) {
- if (err) return this.setState({'unreadMessageCount': 0});
- if (body) {
- var count = parseInt(body.count, 10);
- return this.setState({'unreadMessageCount': count});
- }
- }.bind(this));
- },
handleJoinClick: function (e) {
e.preventDefault();
this.setState({'registrationOpen': true});
@@ -179,7 +170,7 @@ var Navigation = React.createClass({
});
var messageClasses = classNames({
'message-count': true,
- 'show': this.state.unreadMessageCount > 0
+ 'show': this.props.unreadMessageCount > 0
});
var dropdownClasses = classNames({
'user-info': true,
@@ -230,7 +221,7 @@ var Navigation = React.createClass({
href="/messages/"
title={formatMessage({id: 'general.messages'})}>
- {this.state.unreadMessageCount}
+ {this.props.unreadMessageCount}
,
@@ -343,6 +334,7 @@ var mapStateToProps = function (state) {
return {
session: state.session,
permissions: state.permissions,
+ unreadMessageCount: state.messageCount.messageCount,
searchTerm: state.navigation
};
};
diff --git a/src/components/social-message/social-message.jsx b/src/components/social-message/social-message.jsx
new file mode 100644
index 000000000..dfb4adc60
--- /dev/null
+++ b/src/components/social-message/social-message.jsx
@@ -0,0 +1,40 @@
+var classNames = require('classnames');
+var FormattedRelative = require('react-intl').FormattedRelative;
+var React = require('react');
+
+var FlexRow = require('../flex-row/flex-row.jsx');
+
+require('./social-message.scss');
+
+var SocialMessage = React.createClass({
+ type: 'SocialMessage',
+ propTypes: {
+ as: React.PropTypes.string,
+ datetime: React.PropTypes.string.isRequired
+ },
+ getDefaultProps: function () {
+ return {
+ as: 'li'
+ };
+ },
+ render: function () {
+ var classes = classNames(
+ 'social-message',
+ this.props.className
+ );
+ return (
+
+
+
+ {this.props.children}
+
+
+
+
+
+
+ );
+ }
+});
+
+module.exports = SocialMessage;
diff --git a/src/components/social-message/social-message.scss b/src/components/social-message/social-message.scss
new file mode 100644
index 000000000..f6696911d
--- /dev/null
+++ b/src/components/social-message/social-message.scss
@@ -0,0 +1,66 @@
+@import "../../colors";
+@import "../../frameless";
+
+.social-message {
+ margin: 0;
+ border-bottom: 1px solid $ui-border;
+ padding: 1rem;
+ list-style-type: none;
+}
+
+.social-message.mod-unread {
+ background-color: $ui-gray;
+}
+
+.flex-row.mod-social-message {
+ justify-content: space-between;
+ align-items: flex-start;
+}
+
+.social-message-content {
+ max-width: 60%;
+}
+
+a.social-messages-profile-link {
+ color: $type-gray;
+
+ &:hover {
+ color: darken($type-gray, 10);
+ }
+}
+
+.flex-row.mod-comment-message {
+ justify-content: flex-start;
+}
+
+.comment-text {
+ margin-left: 1.5rem;
+}
+
+@media only screen and (max-width: $mobile - 1) {
+ .social-message {
+ text-align: left;
+ }
+
+ .social-message-date {
+ align-self: flex-end;
+ }
+
+ .social-message-content {
+ max-width: 100%;
+ }
+}
+
+@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
+ .social-message {
+ text-align: left;
+ }
+
+ .social-message-date {
+ align-self: flex-end;
+ }
+
+ .social-message-content {
+ max-width: 100%;
+ }
+}
diff --git a/src/lib/api.js b/src/lib/api.js
index 0446c43e8..8ee18706f 100644
--- a/src/lib/api.js
+++ b/src/lib/api.js
@@ -42,18 +42,19 @@ module.exports = function (opts, callback) {
var apiRequest = function (opts) {
if (opts.host !== '') {
- // For IE < 10, we must use XDR for cross-domain requests. XDR does not support
- // custom headers.
- defaults(opts, {useXDR: true});
- if (opts.useXDR) {
+ if ('withCredentials' in new XMLHttpRequest()) {
+ opts.useXDR = false;
+ } else {
+ // For IE < 10, we must use XDR for cross-domain requests. XDR does not support
+ // custom headers.
+ opts.useXDR = true;
delete opts.headers;
- }
- if (opts.authentication) {
- var authenticationParams = ['x-token=' + opts.authentication];
- var parts = opts.uri.split('?');
- var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&');
- opts.uri = parts[0] + '?' + qs;
-
+ if (opts.authentication) {
+ var authenticationParams = ['x-token=' + opts.authentication];
+ var parts = opts.uri.split('?');
+ var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&');
+ opts.uri = parts[0] + '?' + qs;
+ }
}
}
xhr(opts, function (err, res, body) {
diff --git a/src/lib/render.jsx b/src/lib/render.jsx
index 833694552..eddfd707c 100644
--- a/src/lib/render.jsx
+++ b/src/lib/render.jsx
@@ -12,12 +12,7 @@ var reducer = require('../redux/reducer.js');
require('../main.scss');
-var store = redux.createStore(
- reducer,
- redux.applyMiddleware(thunk)
-);
-
-var render = function (jsx, element) {
+var render = function (jsx, element, reducers) {
// Get locale and messages from global namespace (see "init.js")
var locale = window._locale || 'en';
var messages = {};
@@ -33,6 +28,12 @@ var render = function (jsx, element) {
messages = window._messages[locale];
}
+ var allReducers = reducer(reducers);
+ var store = redux.createStore(
+ allReducers,
+ redux.applyMiddleware(thunk)
+ );
+
// Render view component
ReactDOM.render(
diff --git a/src/redux/message-count.js b/src/redux/message-count.js
new file mode 100644
index 000000000..0efa84025
--- /dev/null
+++ b/src/redux/message-count.js
@@ -0,0 +1,70 @@
+var keyMirror = require('keymirror');
+var defaults = require('lodash.defaults');
+
+var api = require('../lib/api');
+
+var Types = keyMirror({
+ SET_MESSAGE_COUNT: null,
+ SET_MESSAGE_COUNT_ERROR: null,
+ SET_STATUS: null
+});
+
+module.exports.getInitialState = function (){
+ return {messageCount: 0};
+};
+
+module.exports.messageCountReducer = function (state, action) {
+ // Reducer for handling changes to session state
+ if (typeof state === 'undefined') {
+ state = module.exports.getInitialState();
+ }
+ switch (action.type) {
+ case Types.SET_MESSAGE_COUNT:
+ return defaults({messageCount: action.count}, state);
+ case Types.SET_STATUS:
+ return defaults({status: action.status}, state);
+ case Types.SET_SESSION_ERROR:
+ // TODO: do something with action.error
+ return state;
+ default:
+ return state;
+ }
+};
+
+module.exports.setSessionError = function (error) {
+ return {
+ type: Types.SET_MESSAGE_COUNT_ERROR,
+ error: error
+ };
+};
+
+module.exports.setCount = function (count) {
+ return {
+ type: Types.SET_MESSAGE_COUNT,
+ count: count
+ };
+};
+
+module.exports.setStatus = function (status){
+ return {
+ type: Types.SET_STATUS,
+ status: status
+ };
+};
+
+module.exports.getCount = function (username) {
+ return function (dispatch) {
+ api({
+ method: 'get',
+ uri: '/users/' + username + '/messages/count'
+ }, function (err, body) {
+ if (err) {
+ dispatch(module.exports.setCount(0));
+ dispatch(module.exports.setSessionError(err));
+ return;
+ }
+ var count = parseInt(body.count, 10);
+ dispatch(module.exports.setCount(count));
+ });
+ };
+};
diff --git a/src/redux/messages.js b/src/redux/messages.js
new file mode 100644
index 000000000..74fa10e49
--- /dev/null
+++ b/src/redux/messages.js
@@ -0,0 +1,245 @@
+var defaultsDeep = require('lodash.defaultsdeep');
+var keyMirror = require('keymirror');
+
+var api = require('../lib/api');
+var log = require('../lib/log');
+
+module.exports.Status = keyMirror({
+ FETCHED: null,
+ NOT_FETCHED: null,
+ FETCHING: null,
+ MESSAGES_ERROR: null,
+ ADMIN_ERROR: null,
+ INVITE_ERROR: null,
+ CLEAR_ERROR: null,
+ DELETE_ERROR: null
+});
+
+module.exports.getInitialState = function () {
+ return {
+ status: {
+ admin: module.exports.Status.NOT_FETCHED,
+ message: module.exports.Status.NOT_FETCHED,
+ clear: module.exports.Status.NOT_FETCHED,
+ delete: module.exports.Status.NOT_FETCHED
+ },
+ messages: {
+ admin: [],
+ social: [],
+ invite: {}
+ }
+ };
+};
+
+module.exports.messagesReducer = function (state, action) {
+ if (typeof state === 'undefined') {
+ state = module.exports.getInitialState();
+ }
+
+ switch (action.type) {
+ case 'SET_MESSAGES':
+ return defaultsDeep({
+ messages: {social: action.messages}
+ }, state);
+ case 'SET_ADMIN_MESSAGES':
+ return defaultsDeep({
+ messages: {admin: action.messages}
+ }, state);
+ case 'SET_MESSAGES_OFFSET':
+ return defaultsDeep({
+ messages: {socialOffset: action.offset}
+ }, state);
+ case 'SET_SCRATCHER_INVITE':
+ return defaultsDeep({
+ messages: {invite: action.invite}
+ }, state);
+ case 'ADMIN_STATUS':
+ return defaultsDeep({status: {admin: action.status}}, state);
+ case 'MESSAGE_STATUS':
+ return defaultsDeep({status: {message: action.status}}, state);
+ case 'CLEAR_STATUS':
+ return defaultsDeep({status: {clear: action.status}}, state);
+ case 'DELETE_STATUS':
+ return defaultsDeep({status: {delete: action.status}}, state);
+ case 'ERROR':
+ log.error(action.error);
+ return state;
+ default:
+ return state;
+ }
+};
+
+module.exports.setMessagesError = function (error) {
+ return {
+ type: 'ERROR',
+ error: error
+ };
+};
+
+module.exports.setMessages = function (messages) {
+ return {
+ type: 'SET_MESSAGES',
+ messages: messages
+ };
+};
+
+module.exports.setMessagesOffset = function (offset) {
+ return {
+ type: 'SET_MESSAGES_OFFSET',
+ offset: offset
+ };
+};
+
+module.exports.setAdminMessages = function (messages) {
+ return {
+ type: 'SET_ADMIN_MESSAGES',
+ messages: messages
+ };
+};
+
+module.exports.setScratcherInvite = function (invite) {
+ return {
+ type: 'SET_SCRATCHER_INVITE',
+ invite: invite
+ };
+};
+
+module.exports.setStatus = function (type, status){
+ return {
+ type: type,
+ status: status
+ };
+};
+
+module.exports.clearMessageCount = function () {
+ return function (dispatch) {
+ dispatch(module.exports.setStatus('CLEAR_STATUS', module.exports.Status.FETCHING));
+ api({
+ host: '',
+ uri: '/site-api/messages/messages-clear/',
+ method: 'POST'
+ }, function (err, body) {
+ if (err) {
+ dispatch(module.exports.setStatus('CLEAR_STATUS', module.exports.Status.CLEAR_ERROR));
+ dispatch(module.exports.setMessagesError(err));
+ return;
+ }
+ if (!body.success) {
+ dispatch(module.exports.setStatus('CLEAR_STATUS', module.exports.Status.CLEAR_ERROR));
+ dispatch(module.exports.setMessagesError('messages not cleared'));
+ return;
+ }
+ dispatch(module.exports.setStatus('CLEAR_STATUS', module.exports.Status.FETCHED));
+ });
+ };
+};
+
+module.exports.clearAdminMessage = function (messageType, messageId, adminMessages) {
+ return function (dispatch) {
+ dispatch(module.exports.setStatus('CLEAR_STATUS', module.exports.Status.FETCHING));
+ api({
+ host: '',
+ uri: '/site-api/messages/messages-delete/',
+ method: 'POST',
+ body: {
+ alertType: messageType,
+ alertId: messageId
+ }
+ }, function (err, body) {
+ if (err) {
+ dispatch(module.exports.setStatus('DELETE_STATUS', module.exports.Status.DELETE_ERROR));
+ dispatch(module.exports.setMessagesError(err));
+ return;
+ }
+ if (!body.success) {
+ dispatch(module.exports.setStatus('DELETE_STATUS', module.exports.Status.DELETE_ERROR));
+ dispatch(module.exports.setMessagesError('messages not cleared'));
+ }
+
+ if (messageType === 'invite') {
+ // invite cleared, so set the invite prop to an empty object
+ dispatch(module.exports.setScratcherInvite({}));
+ } else {
+ // find the admin message and remove it
+ var toRemove = -1;
+ for (var i in adminMessages) {
+ if (adminMessages[i].id === messageId) {
+ toRemove = i;
+ break;
+ }
+ }
+ adminMessages.splice(toRemove, 1);
+ dispatch(module.exports.setAdminMessages(adminMessages));
+ }
+ dispatch(module.exports.setStatus('DELETE_STATUS', module.exports.Status.FETCHED));
+ });
+ };
+};
+
+module.exports.getMessages = function (username, token, messages, offset) {
+ return function (dispatch) {
+ dispatch(module.exports.setStatus('MESSAGE_STATUS', module.exports.Status.FETCHING));
+ api({
+ uri: '/users/' + username + '/messages?limit=40&offset=' + offset,
+ authentication: token
+ }, function (err, body) {
+ if (err) {
+ dispatch(module.exports.setStatus('MESSAGE_STATUS', module.exports.Status.MESSAGES_ERROR));
+ dispatch(module.exports.setMessagesError(err));
+ return;
+ }
+ if (typeof body === 'undefined') {
+ dispatch(module.exports.setStatus('MESSAGE_STATUS', module.exports.Status.MESSAGES_ERROR));
+ dispatch(module.exports.setMessagesError('No session content'));
+ return;
+ }
+ dispatch(module.exports.setStatus('MESSAGE_STATUS', module.exports.Status.FETCHED));
+ dispatch(module.exports.setMessages(messages.concat(body)));
+ dispatch(module.exports.setMessagesOffset(offset + 40));
+ dispatch(module.exports.clearMessageCount(token)); // clear count once messages loaded
+ });
+ };
+};
+
+module.exports.getAdminMessages = function (username, token) {
+ return function (dispatch) {
+ dispatch(module.exports.setStatus('ADMIN_STATUS', module.exports.Status.FETCHING));
+ api({
+ uri: '/users/' + username + '/messages/admin',
+ authentication: token
+ }, function (err, body) {
+ if (err) {
+ dispatch(module.exports.setStatus('ADMIN_STATUS', module.exports.Status.ADMIN_ERROR));
+ dispatch(module.exports.setMessagesError(err));
+ dispatch(module.exports.setAdminMessages([]));
+ return;
+ }
+ if (typeof body === 'undefined') {
+ dispatch(module.exports.setStatus('ADMIN_STATUS', module.exports.Status.ADMIN_ERROR));
+ dispatch(module.exports.setMessagesError('No session content'));
+ dispatch(module.exports.setAdminMessages([]));
+ return;
+ }
+ dispatch(module.exports.setAdminMessages(body));
+ dispatch(module.exports.setStatus('ADMIN_STATUS', module.exports.Status.FETCHED));
+ });
+ };
+};
+
+module.exports.getScratcherInvite = function (username, token) {
+ return function (dispatch) {
+ api({
+ uri: '/users/' + username + '/invites',
+ authentication: token
+ }, function (err, body) {
+ if (err) {
+ dispatch(module.exports.setStatus('ADMIN_STATUS', module.exports.Status.INVITE_ERROR));
+ dispatch(module.exports.setMessagesError(err));
+ dispatch(module.exports.setScratcherInvite({}));
+ return;
+ }
+ if (typeof body === 'undefined') return dispatch(module.exports.setMessagesError('No session content'));
+ dispatch(module.exports.setScratcherInvite(body));
+ });
+ };
+};
diff --git a/src/redux/reducer.js b/src/redux/reducer.js
index 1531c308b..b1f18e77f 100644
--- a/src/redux/reducer.js
+++ b/src/redux/reducer.js
@@ -1,17 +1,24 @@
var combineReducers = require('redux').combineReducers;
-var scheduleReducer = require('./conference-schedule.js').scheduleReducer;
-var detailsReducer = require('./conference-details.js').detailsReducer;
+var messageCountReducer = require('./message-count.js').messageCountReducer;
var permissionsReducer = require('./permissions.js').permissionsReducer;
var sessionReducer = require('./session.js').sessionReducer;
-var navigationReducer = require('./navigation.js').navigationReducer;
-var appReducer = combineReducers({
- session: sessionReducer,
- permissions: permissionsReducer,
- conferenceSchedule: scheduleReducer,
- conferenceDetails: detailsReducer,
- navigation: navigationReducer
-});
-
-module.exports = appReducer;
+/**
+ * Returns a combined reducer to be used for a page in `render.jsx`.
+ * The reducers used globally are applied here - session and permissions
+ * - and any reducers specific to the page should be passed into
+ * `render()` as an object (which will then be passed to the function
+ * below).
+ * @param {Object} opts key/value where the key is the name of the
+ * redux state, value is the reducer function.
+ * @return {Object} combined reducer to be used in the redux store
+ */
+module.exports = function (opts) {
+ opts = opts || {};
+ return combineReducers(Object.assign(opts, {
+ session: sessionReducer,
+ permissions: permissionsReducer,
+ messageCount: messageCountReducer
+ }));
+};
diff --git a/src/redux/session.js b/src/redux/session.js
index e0a2bbbee..9addc58b1 100644
--- a/src/redux/session.js
+++ b/src/redux/session.js
@@ -2,6 +2,7 @@ var keyMirror = require('keymirror');
var defaults = require('lodash.defaults');
var api = require('../lib/api');
+var messageCountActions = require('./message-count.js');
var permissionsActions = require('./permissions.js');
var Types = keyMirror({
@@ -96,6 +97,7 @@ module.exports.refreshSession = function () {
// get the permissions from the updated session
dispatch(permissionsActions.storePermissions(body.permissions));
+ dispatch(messageCountActions.getCount(body.user.username));
return;
}
});
diff --git a/src/routes.json b/src/routes.json
index d0fd1ac04..7852dadee 100644
--- a/src/routes.json
+++ b/src/routes.json
@@ -141,6 +141,13 @@
"view": "jobs/moderator/moderator",
"title": "Community Moderator"
},
+ {
+ "name": "messages",
+ "pattern": "^/messages/?$",
+ "routeAlias": "/messages",
+ "view": "messages/container",
+ "title": "Messages"
+ },
{
"name": "microworld-art",
"pattern": "^/microworlds/art",
diff --git a/src/views/conference/2016/details/details.jsx b/src/views/conference/2016/details/details.jsx
index 1584516c1..3dfbf3871 100644
--- a/src/views/conference/2016/details/details.jsx
+++ b/src/views/conference/2016/details/details.jsx
@@ -91,4 +91,8 @@ var mapStateToProps = function (state) {
var ConnectedDetails = connect(mapStateToProps)(ConferenceDetails);
-render(, document.getElementById('app'));
+render(
+ ,
+ document.getElementById('app'),
+ {conferenceDetails: detailsActions.detailsReducer}
+);
diff --git a/src/views/conference/2016/schedule/schedule.jsx b/src/views/conference/2016/schedule/schedule.jsx
index 8cafb8ea2..71a9a8ef9 100644
--- a/src/views/conference/2016/schedule/schedule.jsx
+++ b/src/views/conference/2016/schedule/schedule.jsx
@@ -128,4 +128,8 @@ var mapStateToProps = function (state) {
var ConnectedSchedule = connect(mapStateToProps)(ConferenceSchedule);
-render(, document.getElementById('app'));
+render(
+ ,
+ document.getElementById('app'),
+ {conferenceSchedule: scheduleActions.scheduleReducer}
+);
diff --git a/src/views/messages/container.jsx b/src/views/messages/container.jsx
new file mode 100644
index 000000000..ac1098f84
--- /dev/null
+++ b/src/views/messages/container.jsx
@@ -0,0 +1,168 @@
+var connect = require('react-redux').connect;
+var React = require('react');
+
+var messageActions = require('../../redux/messages.js');
+var render = require('../../lib/render.jsx');
+var sessionActions = require('../../redux/session.js');
+
+var Page = require('../../components/page/www/page.jsx');
+var MessagesPresentation = require('./presentation.jsx');
+
+var Messages = React.createClass({
+ type: 'ConnectedMessages',
+ getInitialState: function () {
+ return {
+ filterValues: [],
+ displayedMessages: []
+ };
+ },
+ getDefaultProps: function () {
+ return {
+ sessionStatus: sessionActions.Status.NOT_FETCHED,
+ user: {},
+ flags: {},
+ messageOffset: 0,
+ numNewMessages: 0
+ };
+ },
+ componentDidUpdate: function (prevProps) {
+ if (this.props.user != prevProps.user) {
+ if (this.props.user.token) {
+ this.props.dispatch(
+ messageActions.getMessages(
+ this.props.user.username,
+ this.props.user.token,
+ this.props.messages,
+ this.props.messageOffset
+ )
+ );
+ this.props.dispatch(
+ messageActions.getAdminMessages(
+ this.props.user.username, this.props.user.token, this.props.messageOffset
+ )
+ );
+ this.props.dispatch(
+ messageActions.getScratcherInvite(this.props.user.username, this.props.user.token)
+ );
+ }
+ }
+ },
+ componentDidMount: function () {
+ if (this.props.user.token) {
+ this.props.dispatch(
+ messageActions.getMessages(
+ this.props.user.username,
+ this.props.user.token,
+ this.props.messages,
+ this.props.messageOffset
+ )
+ );
+ this.props.dispatch(
+ messageActions.getAdminMessages(
+ this.props.user.username, this.props.user.token, this.props.messageOffset
+ )
+ );
+ this.props.dispatch(
+ messageActions.getScractherInvite(this.props.user.username, this.props.user.token)
+ );
+ }
+ },
+ handleFilterClick: function (field, choice) {
+ switch (choice) {
+ case 'comments':
+ return this.setState({filterValues: ['addcomment']});
+ case 'projects':
+ return this.setState({filterValues: [
+ 'loveproject',
+ 'favoriteproject',
+ 'remixproject'
+ ]});
+ case 'studios':
+ return this.setState({filterValues: [
+ 'curatorinvite',
+ 'studioactivity',
+ 'becomeownerstudio'
+ ]});
+ case 'forums':
+ return this.setState({filterValues: ['forumpost']});
+ default:
+ return this.setState({filterValues: []});
+ }
+ },
+ handleMessageDismiss: function (messageType, messageId) {
+ var adminMessages = null;
+ if (messageType === 'notification') {
+ adminMessages = this.props.adminMessages;
+ }
+ this.props.dispatch(
+ messageActions.clearAdminMessage(messageType, messageId, adminMessages)
+ );
+ },
+ handleLoadMoreMessages: function () {
+ this.props.dispatch(
+ messageActions.getMessages(
+ this.props.user.username,
+ this.props.user.token,
+ this.props.messages,
+ this.props.messageOffset
+ )
+ );
+ },
+ filterMessages: function (messages, typesAllowed) {
+ var filteredMessages = [];
+ for (var i in messages) {
+ if (typesAllowed.indexOf(messages[i].type) > -1) {
+ filteredMessages.push(messages[i]);
+ }
+ }
+ return filteredMessages;
+ },
+ render: function () {
+ var loadMore = true;
+ if (this.props.messageOffset > this.props.messages.length && this.props.messageOffset > 0) {
+ loadMore = false;
+ }
+
+ var messages = this.props.messages;
+ if (this.state.filterValues.length > 0) {
+ messages = this.filterMessages(messages, this.state.filterValues);
+ }
+
+ return(
+
+ );
+ }
+});
+
+var mapStateToProps = function (state) {
+ return {
+ sessionStatus: state.session.status,
+ user: state.session.session.user,
+ flags: state.session.session.flags,
+ numNewMessages: state.messageCount.messageCount,
+ messages: state.messages.messages.social,
+ adminMessages: state.messages.messages.admin,
+ invite: state.messages.messages.invite,
+ messageOffset: state.messages.messages.socialOffset,
+ requestStatus: state.messages.status
+ };
+};
+
+var ConnectedMessages = connect(mapStateToProps)(Messages);
+render(
+ ,
+ document.getElementById('app'),
+ {messages: messageActions.messagesReducer}
+);
diff --git a/src/views/messages/l10n.json b/src/views/messages/l10n.json
new file mode 100644
index 000000000..b145055ca
--- /dev/null
+++ b/src/views/messages/l10n.json
@@ -0,0 +1,30 @@
+{
+ "messages.activityAll": "All Activity",
+ "messages.activityComments": "Comment Activity",
+ "messages.activityProjects": "Project Activity",
+ "messages.activityStudios": "Studio Activity",
+ "messages.activityForums": "Forum Activity",
+ "messages.becomeManagerText": "{username} promoted you to manager for the studio {studio}",
+ "messages.curatorInviteText": "{actorLink} invited you to curate the studio {studioLink}. Visit the {tabLink} on the studio to accept the invitation",
+ "messages.curatorTabText": "curator tab",
+ "messages.favoriteText": "{profileLink} favorited your project {projectLink}",
+ "messages.filterBy": "Filter by",
+ "messages.followText": "{profileLink} is now following you",
+ "messages.forumPostText": "There are new posts in the forum thread: {topicLink}",
+ "messages.learnMore": "Click here to learn more",
+ "messages.loveText": "{profileLink} loved your project {projectLink}",
+ "messages.messageTitle": "Messages",
+ "messages.profileComment": "{profileLink} commented on {commentLink}",
+ "messages.commentReply": "{profileLink} replied to your comment on {commentLink}",
+ "messages.profileOther": "{username}'s reply",
+ "messages.profileSelf": "your profile",
+ "messages.projectComment": "{profileLink} commented on your project {commentLink}",
+ "messages.remixText": "{profileLink} remixed your project {remixedProjectLink} as {projectLink}",
+ "messages.scratcherInvite": "You are invited to become a Scratcher! {learnMore}!",
+ "messages.scratchTeamTitle": "Messages from the Scratch Team",
+ "messages.studioActivityText": "There was new activity in {studioLink} today",
+ "messages.studioCommentReply": "{profileLink} replied to your comment in {commentLink}",
+ "messages.userJoinText": "Welcome to Scratch! After you make projects and comments, you'll get messages about them here. Go {exploreLink} or {makeProjectLink}.",
+ "messages.userJoinMakeProject": "make a project",
+ "messages.requestError": "oops! Looks like there was a problem getting some of your messages. Please try to reload this page"
+}
diff --git a/src/views/messages/message-rows/admin-message.jsx b/src/views/messages/message-rows/admin-message.jsx
new file mode 100644
index 000000000..903a85970
--- /dev/null
+++ b/src/views/messages/message-rows/admin-message.jsx
@@ -0,0 +1,42 @@
+var FormattedDate = require('react-intl').FormattedDate;
+var React = require('react');
+
+var Button = require('../../../components/forms/button.jsx');
+var FlexRow = require('../../../components/flex-row/flex-row.jsx');
+
+var AdminMessage = React.createClass({
+ type: 'AdminMessage',
+ propTypes: {
+ id: React.PropTypes.number.isRequired,
+ message: React.PropTypes.string.isRequired,
+ datetimeCreated: React.PropTypes.string.isRequired,
+ onDismiss: React.PropTypes.func.isRequired
+ },
+ render: function () {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+});
+
+module.exports = AdminMessage;
diff --git a/src/views/messages/message-rows/become-manager.jsx b/src/views/messages/message-rows/become-manager.jsx
new file mode 100644
index 000000000..f344d197c
--- /dev/null
+++ b/src/views/messages/message-rows/become-manager.jsx
@@ -0,0 +1,45 @@
+var classNames = require('classnames');
+var FormattedMessage = require('react-intl').FormattedMessage;
+var React = require('react');
+
+var SocialMessage = require('../../../components/social-message/social-message.jsx');
+
+var BecomeManagerMessage = React.createClass({
+ type: 'BecomeManagerMessage',
+ propTypes: {
+ actorUsername: React.PropTypes.string.isRequired,
+ studioId: React.PropTypes.number.isRequired,
+ studioTitle: React.PropTypes.string.isRequired,
+ datetimePromoted: React.PropTypes.string.isRequired
+ },
+ render: function () {
+ var actorUri = '/users/' + this.props.actorUsername + '/';
+ var studioUri = '/studios/' + this.props.studioId + '/';
+
+ var classes = classNames(
+ 'mod-become-manager',
+ this.props.className
+ );
+ return (
+
+
+ {this.props.actorUsername}
+ ,
+ studio: {this.props.studioTitle}
+ }}
+ />
+
+ );
+ }
+});
+
+module.exports = BecomeManagerMessage;
diff --git a/src/views/messages/message-rows/comment-message.jsx b/src/views/messages/message-rows/comment-message.jsx
new file mode 100644
index 000000000..7e435213c
--- /dev/null
+++ b/src/views/messages/message-rows/comment-message.jsx
@@ -0,0 +1,161 @@
+var classNames = require('classnames');
+var FormattedMessage = require('react-intl').FormattedMessage;
+var injectIntl = require('react-intl').injectIntl;
+var React = require('react');
+
+var Comment = require('../../../components/comment/comment.jsx');
+var FlexRow = require('../../../components/flex-row/flex-row.jsx');
+var SocialMessage = require('../../../components/social-message/social-message.jsx');
+
+var CommentMessage = injectIntl(React.createClass({
+ type: 'CommentMessage',
+ propTypes: {
+ actorUsername: React.PropTypes.string.isRequired,
+ actorId: React.PropTypes.number.isRequired,
+ objectType: React.PropTypes.oneOf([0, 1, 2]).isRequired,
+ objectId: React.PropTypes.number.isRequired,
+ commentId: React.PropTypes.number.isRequired,
+ commentText: React.PropTypes.string.isRequired,
+ commentDateTime: React.PropTypes.string.isRequired,
+ objectTitle: React.PropTypes.string,
+ commentee: React.PropTypes.string
+ },
+ getObjectLink: function (objectType, commentId, objectId) {
+ switch (objectType) {
+ case 0:
+ return '/projects/' + objectId + '/#comments-' + commentId;
+ case 1:
+ return '/users/' + objectId + '/#comments-' + commentId;
+ case 2:
+ return '/studios/' + objectId + '/comments/#comments-' + commentId;
+ }
+ },
+ getMessageText: function (objectType, commentee) {
+ var actorLink = '/users/' + this.props.actorUsername + '/';
+ if (objectType === 2) {
+ // studio comment notifications only occur for direct replies
+ if (typeof commentee !== 'undefined' && commentee === this.props.user.username) {
+ var commentLink = '/studios/' + this.props.objectId + '/comments/#comments-' + this.props.commentId;
+ return