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 + {this.props.actorUsername} + , + commentLink: {this.props.objectTitle} + }} + />; + } + } else if (objectType === 1) { + var profileLink = '/users/' + this.props.objectId + '/#comments-' + this.props.commentId; + var linkText = ''; + if (typeof commentee !== 'undefined' && commentee === this.props.user.username) { + // is a profile comment, and is a reply + if (this.props.objectTitle === this.props.user.username) { + linkText = this.props.intl.formatMessage({ + id: 'messages.profileSelf' + }); + } else { + linkText = this.props.intl.formatMessage({ + id: 'messages.profileOther', + values: { + username: this.props.objectId + } + }); + } + return + {this.props.actorUsername} + , + commentLink: {linkText} + }} + />; + } else { + // is a profile comment and not a reply, must be own profile + linkText = this.props.intl.formatMessage({ + id: 'messages.profileSelf' + }); + return + {this.props.actorUsername} + , + commentLink: {linkText} + }} + />; + } + } else { + var projectLink = '/projects/' + this.props.objectId + '/#comments-' + this.props.commentId; + // must be a project comment, since it's not the other two, and the strict prop type reqs + if (typeof commentee !== 'undefined' && commentee === this.props.user.username) { + return + {this.props.actorUsername} + , + commentLink: {this.props.objectTitle} + }} + />; + } else { + return + {this.props.actorUsername} + , + commentLink: {this.props.objectTitle} + }} + />; + } + } + }, + render: function () { + var messageText = this.getMessageText(this.props.objectType, this.props.commentee); + var commentorAvatar = 'https://cdn2.scratch.mit.edu/get_image/user/' + this.props.actorId + '_32x32.png'; + var commentorAvatarAlt = this.props.actorUsername + '\'s avatar'; + + var classes = classNames( + 'mod-comment-message', + this.props.className + ); + return ( + +

    {messageText}

    + + {commentorAvatarAlt} + + +
    + ); + } +})); + +module.exports = CommentMessage; diff --git a/src/views/messages/message-rows/curator-invite.jsx b/src/views/messages/message-rows/curator-invite.jsx new file mode 100644 index 000000000..4b49e1b79 --- /dev/null +++ b/src/views/messages/message-rows/curator-invite.jsx @@ -0,0 +1,48 @@ +var classNames = require('classnames'); +var FormattedMessage = require('react-intl').FormattedMessage; +var injectIntl = require('react-intl').injectIntl; +var React = require('react'); + +var SocialMessage = require('../../../components/social-message/social-message.jsx'); + +var CuratorInviteMessage = injectIntl(React.createClass({ + type: 'CuratorInviteMessage', + propTypes: { + actorUsername: React.PropTypes.string.isRequired, + studioId: React.PropTypes.number.isRequired, + studioTitle: React.PropTypes.string.isRequired, + datetimePromoted: React.PropTypes.string.isRequired + }, + render: function () { + var studioLink = '/studios/' + this.props.studioId + '/'; + var actorLink = '/users/' + this.props.actorUsername + '/'; + var tabText = this.props.intl.formatMessage({id: 'messages.curatorTabText'}); + + var classes = classNames( + 'mod-curator-invite', + this.props.className + ); + return ( + + + {this.props.actorUsername} + , + studioLink: {this.props.studioTitle}, + tabLink: {tabText} + }} + /> + + ); + } +})); + +module.exports = CuratorInviteMessage; diff --git a/src/views/messages/message-rows/favorite-project.jsx b/src/views/messages/message-rows/favorite-project.jsx new file mode 100644 index 000000000..c8bf62372 --- /dev/null +++ b/src/views/messages/message-rows/favorite-project.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 FavoriteProjectMessage = React.createClass({ + type: 'FavoriteProjectMessage', + propTypes: { + actorUsername: React.PropTypes.string.isRequired, + projectId: React.PropTypes.number.isRequired, + projectTitle: React.PropTypes.string.isRequired, + favoriteDateTime: React.PropTypes.string.isRequired + }, + render: function () { + var projectLink = '/projects/' + this.props.projectId; + var profileLink = '/users/' + this.props.actorUsername; + + var classes = classNames( + 'mod-love-favorite', + this.props.className + ); + return ( + + + {this.props.actorUsername} + , + projectLink: {this.props.projectTitle} + }} + /> + + ); + } +}); + +module.exports = FavoriteProjectMessage; diff --git a/src/views/messages/message-rows/follow-user.jsx b/src/views/messages/message-rows/follow-user.jsx new file mode 100644 index 000000000..00bfaf436 --- /dev/null +++ b/src/views/messages/message-rows/follow-user.jsx @@ -0,0 +1,41 @@ +var classNames = require('classnames'); +var FormattedMessage = require('react-intl').FormattedMessage; +var React = require('react'); + +var SocialMessage = require('../../../components/social-message/social-message.jsx'); + +var FollowUserMessage = React.createClass({ + type: 'FollowUserMessage', + propTypes: { + followerUsername: React.PropTypes.string.isRequired, + followDateTime: React.PropTypes.string.isRequired + }, + render: function () { + var profileLink = '/users/' + this.props.followerUsername; + '/'; + + var classes = classNames( + 'mod-follow-user', + this.props.className + ); + return ( + + + {this.props.followerUsername} + + }} + /> + + ); + } +}); + +module.exports = FollowUserMessage; diff --git a/src/views/messages/message-rows/forum-topic-post.jsx b/src/views/messages/message-rows/forum-topic-post.jsx new file mode 100644 index 000000000..083d038bb --- /dev/null +++ b/src/views/messages/message-rows/forum-topic-post.jsx @@ -0,0 +1,38 @@ +var classNames = require('classnames'); +var FormattedMessage = require('react-intl').FormattedMessage; +var React = require('react'); + +var SocialMessage = require('../../../components/social-message/social-message.jsx'); + +var ForumPostMessage = React.createClass({ + type: 'ForumPostMessage', + propTypes: { + actorUsername: React.PropTypes.string.isRequired, + topicId: React.PropTypes.number.isRequired, + topicTitle: React.PropTypes.string.isRequired, + datetimeCreated: React.PropTypes.string.isRequired + }, + render: function () { + var topicLink = '/discuss/topic/' + this.props.topicId + '/unread/'; + + var classes = classNames( + 'mod-studio-activity', + this.props.className + ); + return ( + + {this.props.topicTitle} + }} + /> + + ); + } +}); + +module.exports = ForumPostMessage; diff --git a/src/views/messages/message-rows/love-project.jsx b/src/views/messages/message-rows/love-project.jsx new file mode 100644 index 000000000..9c7cfec02 --- /dev/null +++ b/src/views/messages/message-rows/love-project.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 LoveProjectMessage = React.createClass({ + type: 'LoveProjectMessage', + propTypes: { + actorUsername: React.PropTypes.string.isRequired, + projectId: React.PropTypes.number.isRequired, + projectTitle: React.PropTypes.string.isRequired, + loveDateTime: React.PropTypes.string.isRequired + }, + render: function () { + var projectLink = '/projects/' + this.props.projectId; + var profileLink = '/users/' + this.props.actorUsername; + + var classes = classNames( + 'mod-love-project', + this.props.className + ); + return ( + + + {this.props.actorUsername} + , + projectLink: {this.props.projectTitle} + }} + /> + + ); + } +}); + +module.exports = LoveProjectMessage; diff --git a/src/views/messages/message-rows/remix-project.jsx b/src/views/messages/message-rows/remix-project.jsx new file mode 100644 index 000000000..b11a5b512 --- /dev/null +++ b/src/views/messages/message-rows/remix-project.jsx @@ -0,0 +1,49 @@ +var classNames = require('classnames'); +var FormattedMessage = require('react-intl').FormattedMessage; +var React = require('react'); + +var SocialMessage = require('../../../components/social-message/social-message.jsx'); + +var RemixProjectMessage = React.createClass({ + type: 'RemixProjectMessage', + propTypes: { + actorUsername: React.PropTypes.string.isRequired, + projectId: React.PropTypes.number.isRequired, + projectTitle: React.PropTypes.string.isRequired, + parentId: React.PropTypes.number.isRequired, + parentTitle: React.PropTypes.string.isRequired, + remixDate: React.PropTypes.string.isRequired + }, + render: function () { + var projectLink = '/projects/' + this.props.projectId; + var profileLink = '/users/' + this.props.actorUsername; + var remixedProjectLink = '/projects/' + this.props.parentId; + + var classes = classNames( + 'mod-remix-project', + this.props.className + ); + return ( + + + {this.props.actorUsername} + , + projectLink: {this.props.projectTitle}, + remixedProjectLink: {this.props.parentTitle} + }} + /> + + ); + } +}); + +module.exports = RemixProjectMessage; diff --git a/src/views/messages/message-rows/scratcher-invite.jsx b/src/views/messages/message-rows/scratcher-invite.jsx new file mode 100644 index 000000000..fd7eab9f0 --- /dev/null +++ b/src/views/messages/message-rows/scratcher-invite.jsx @@ -0,0 +1,50 @@ +var FormattedDate = require('react-intl').FormattedDate; +var FormattedMessage = require('react-intl').FormattedMessage; +var injectIntl = require('react-intl').injectIntl; +var React = require('react'); + +var Button = require('../../../components/forms/button.jsx'); +var FlexRow = require('../../../components/flex-row/flex-row.jsx'); + +var AdminMessage = injectIntl(React.createClass({ + type: 'AdminMessage', + propTypes: { + id: React.PropTypes.number.isRequired, + username: React.PropTypes.string.isRequired, + datetimeCreated: React.PropTypes.string.isRequired, + onDismiss: React.PropTypes.func.isRequired + }, + render: function () { + var learnMoreLink = '/users/' + this.props.username + '#promote'; + var learnMoreMessage = this.props.intl.formatMessage({id: 'messages.learnMore'}); + return ( +
  • + + + + + + +

    + {learnMoreMessage} + }} + /> +

    +
  • + ); + } +})); + +module.exports = AdminMessage; diff --git a/src/views/messages/message-rows/studio-activity.jsx b/src/views/messages/message-rows/studio-activity.jsx new file mode 100644 index 000000000..c68eed5e8 --- /dev/null +++ b/src/views/messages/message-rows/studio-activity.jsx @@ -0,0 +1,37 @@ +var classNames = require('classnames'); +var FormattedMessage = require('react-intl').FormattedMessage; +var React = require('react'); + +var SocialMessage = require('../../../components/social-message/social-message.jsx'); + +var StudioActivityMessage = React.createClass({ + type: 'StudioActivityMessage', + propTypes: { + studioId: React.PropTypes.number.isRequired, + studioTitle: React.PropTypes.string.isRequired, + datetimeCreated: React.PropTypes.string.isRequired + }, + render: function () { + var studioLink = '/studios/' + this.props.studioId; + + var classes = classNames( + 'mod-studio-activity', + this.props.className + ); + return ( + + {this.props.studioTitle} + }} + /> + + ); + } +}); + +module.exports = StudioActivityMessage; diff --git a/src/views/messages/message-rows/user-join.jsx b/src/views/messages/message-rows/user-join.jsx new file mode 100644 index 000000000..a64b4fa1b --- /dev/null +++ b/src/views/messages/message-rows/user-join.jsx @@ -0,0 +1,38 @@ +var classNames = require('classnames'); +var FormattedMessage = require('react-intl').FormattedMessage; +var injectIntl = require('react-intl').injectIntl; +var React = require('react'); + +var SocialMessage = require('../../../components/social-message/social-message.jsx'); + +var UserJoinMessage = injectIntl(React.createClass({ + type: 'UserJoinMessage', + propTypes: { + datetimeJoined: React.PropTypes.string.isRequired + }, + render: function () { + var exploreText = this.props.intl.formatMessage({id: 'general.explore'}); + var projectText = this.props.intl.formatMessage({id: 'messages.userJoinMakeProject'}); + + var classes = classNames( + 'mod-user-join', + this.props.className + ); + return ( + + {exploreText}, + makeProjectLink: {projectText} + }} + /> + + ); + } +})); + +module.exports = UserJoinMessage; diff --git a/src/views/messages/messages.scss b/src/views/messages/messages.scss new file mode 100644 index 000000000..e341fc201 --- /dev/null +++ b/src/views/messages/messages.scss @@ -0,0 +1,127 @@ +@import "../../colors"; +@import "../../frameless"; + +#view { + background-color: $ui-light-gray; + padding: 0; +} + +.title-banner.mod-messages { + background-color: $ui-blue; + color: $type-white; +} + +.flex-row.mod-messages-title { + justify-content: space-between; +} + +.title-banner-h1.mod-messages { + margin: 0; + text-align: left; +} + +.form-control { + color: $type-gray; +} + +.help-block { + display: none; +} + +.messages-admin, +.messages-social { + margin-bottom: 3rem; +} + +.messages-admin-list, +.messages-social-list { + padding: 0; + list-style-type: none; +} + +.admin-message { + border: 1px solid darken($ui-gray, 10); + border-radius: 5px; + background-color: lighten($ui-blue, 40); + padding: 1rem; +} + +.admin-message-header { + justify-content: space-between; + align-items: center; +} + +.admin-message-content { + margin: 0; +} + +.admin-message-date { + font-size: .8rem; +} + +.button.mod-scratcher-invite-dismiss, +.button.mod-admin-message-dismiss { + box-shadow: none; + padding: .25rem; + line-height: .8rem; + + &:hover { + box-shadow: none; + } +} + +.messages-social-title-unread { + margin-left: 1rem; + border-radius: 1rem; + background-color: $ui-orange; + padding: .25rem .5rem; + color: $type-white; +} + +.messages-social-list { + border: 1px solid $ui-border; + border-bottom-width: 0; + border-radius: 5px; + background-color: $ui-white; +} + +.comment-message-info { + margin-top: 0; +} + +.comment-text { + background-color: $ui-white; +} + +.flex-row.mod-comment-message { + margin-bottom: .25rem; + align-items: flex-start; +} + +.comment-message-info-img { + width: 40px; + height: 40px; +} + +.messages-social-loadmore { + display: block; + margin: 1rem auto; +} + +@media only screen and (max-width: $mobile - 1) { + .flex-row.admin-message-header, + .flex-row.mod-comment-message { + flex-direction: row; + } + + .comment-text { + max-width: 60%; + } +} + +@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { + .flex-row.admin-message-header, + .flex-row.mod-comment-message { + flex-direction: row; + } +} diff --git a/src/views/messages/presentation.jsx b/src/views/messages/presentation.jsx new file mode 100644 index 000000000..60eb6ca2c --- /dev/null +++ b/src/views/messages/presentation.jsx @@ -0,0 +1,321 @@ +var FormattedMessage = require('react-intl').FormattedMessage; +var FormattedNumber = require('react-intl').FormattedNumber; +var injectIntl = require('react-intl').injectIntl; +var React = require('react'); + +var Button = require('../../components/forms/button.jsx'); +var FlexRow = require('../../components/flex-row/flex-row.jsx'); +var Form = require('../../components/forms/form.jsx'); +var Select = require('../../components/forms/select.jsx'); +var TitleBanner = require('../../components/title-banner/title-banner.jsx'); + +var messageStatuses = require('../../redux/messages').Status; + +// Message Components +var AdminMessage = require('./message-rows/admin-message.jsx'); +var BecomeManagerMessage = require('./message-rows/become-manager.jsx'); +var CommentMessage = require('./message-rows/comment-message.jsx'); +var CuratorInviteMessage = require('./message-rows/curator-invite.jsx'); +var FavoriteProjectMessage = require('./message-rows/favorite-project.jsx'); +var FollowUserMessage = require('./message-rows/follow-user.jsx'); +var ForumPostMessage= require('./message-rows/forum-topic-post.jsx'); +var LoveProjectMessage = require('./message-rows/love-project.jsx'); +var RemixProjectMessage = require('./message-rows/remix-project.jsx'); +var ScratcherInvite = require('./message-rows/scratcher-invite.jsx'); +var StudioActivityMessage = require('./message-rows/studio-activity.jsx'); +var UserJoinMessage = require('./message-rows/user-join.jsx'); + +require('./messages.scss'); + +var SocialMessagesList = React.createClass({ + type: 'SocialMessagesList', + propTypes: { + loadStatus: React.PropTypes.string, + messages: React.PropTypes.array.isRequired, + numNewMessages: React.PropTypes.number, + loadMore: React.PropTypes.bool.isRequired, + loadMoreMethod: React.PropTypes.func + }, + getDefaultProps: function () { + return { + loadStatus: messageStatuses.FETCHING, + numNewMessages: 0 + }; + }, + getComponentForMessage: function (message, unread) { + var className = (unread) ? 'mod-unread' : ''; + var key = message.type + '_' + message.id; + + switch (message.type) { + case 'followuser': + return ; + case 'loveproject': + return ; + case 'favoriteproject': + return ; + case 'addcomment': + return ; + case 'curatorinvite': + return ; + case 'remixproject': + return ; + case 'studioactivity': + return ; + case 'forumpost': + return ; + case 'becomeownerstudio': + return ; + case 'userjoin': + return ; + } + }, + renderSocialMessages: function (messages, unreadCount) { + var messageList = []; + for (var i in messages) { + if (i <= unreadCount) { + messageList.push(this.getComponentForMessage(messages[i], true)); + } else { + messageList.push(this.getComponentForMessage(messages[i], false)); + } + } + return messageList; + }, + renderLoadMore: function (loadMore) { + if (loadMore) { + return ; + } + return null; + }, + render: function () { + if (this.props.loadStatus === messageStatuses.MESSAGES_ERROR) { + return ( +
    +
    +

    + +

    +
    +

    +
    + ); + } + + return ( +
    + {this.props.messages.length > 0 ? [ +
    +

    + + + + +

    +
    , +
      + {this.renderSocialMessages(this.props.messages, (this.props.numNewMessages - 1))} +
    , + this.renderLoadMore(this.props.loadMore) + ] : []} +
    + ); + } +}); + +var MessagesPresentation = injectIntl(React.createClass({ + type: 'MessagesPresentation', + propTypes: { + sessionStatus: React.PropTypes.string.isRequired, + user: React.PropTypes.object.isRequired, + messages: React.PropTypes.array.isRequired, + adminMessages: React.PropTypes.array.isRequired, + scratcherInvite: React.PropTypes.object.isRequired, + numNewMessages: React.PropTypes.number, + handleFilterClick: React.PropTypes.func.isRequired, + handleAdminDismiss: React.PropTypes.func.isRequired, + loadMore: React.PropTypes.bool.isRequired, + loadMoreMethod: React.PropTypes.func, + requestStatus: React.PropTypes.object.isRequired + }, + getDefaultProps: function () { + return { + numNewMessages: 0, + filterOpen: false + }; + }, + render: function () { + var adminMessageLength = this.props.adminMessages.length; + if (Object.keys(this.props.scratcherInvite).length > 0) { + adminMessageLength = adminMessageLength + 1; + } + + return ( +
    + + +

    + +

    +
    +
    +