GH-1361: Implement Notifications Page (#1487)

* start work on www page
committing out of paranoia.
including changing splash page endpoints

* updates from feedback

thanks @rschamp! This includes:

1. splitting out messages list into a separate component (for clarity)
2. some comment/formatting adjustments for the api calls
3. removal of an extraneous property in emoji-text

* remove duplicate string declaration

* use object.assign instead of defaults deep

we don’t need deep defaults

* fix react warnings
This commit is contained in:
Matthew Taylor 2017-08-31 17:05:22 -04:00 committed by GitHub
parent a089b664da
commit 3dd768f2f6
63 changed files with 1906 additions and 65 deletions

View file

@ -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 (
<div className={classes}>
<EmojiText className="mod-comment" text={this.props.comment} />
{typeof this.props.datetimeCreated !== 'undefined' ? [
<p className="comment-text-timestamp">
<FormattedRelative value={new Date(this.props.datetimeCreated)} />
</p>
] : []}
</div>
);
}
});
module.exports = CommentText;

View file

@ -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;
}

View file

@ -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 (
<this.props.as
className={classes}
dangerouslySetInnerHTML={{
__html: this.props.text
}}
/>
);
}
});
module.exports = EmojiText;

View file

@ -0,0 +1,4 @@
.emoji {
max-width: 24px;
vertical-align: middle;
}

View file

@ -5,6 +5,7 @@ var ReactIntl = require('react-intl');
var FormattedMessage = ReactIntl.FormattedMessage; var FormattedMessage = ReactIntl.FormattedMessage;
var injectIntl = ReactIntl.injectIntl; var injectIntl = ReactIntl.injectIntl;
var messageCountActions = require('../../../redux/message-count.js');
var sessionActions = require('../../../redux/session.js'); var sessionActions = require('../../../redux/session.js');
var api = require('../../../lib/api'); var api = require('../../../lib/api');
@ -30,20 +31,21 @@ var Navigation = React.createClass({
loginOpen: false, loginOpen: false,
loginError: null, loginError: null,
registrationOpen: false, registrationOpen: false,
unreadMessageCount: 0, // bubble number to display how many notifications someone has.
messageCountIntervalId: -1 // javascript method interval id for getting messsage count. messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
}; };
}, },
getDefaultProps: function () { getDefaultProps: function () {
return { return {
session: {}, session: {},
unreadMessageCount: 0, // bubble number to display how many notifications someone has.
searchTerm: '' searchTerm: ''
}; };
}, },
componentDidMount: function () { componentDidMount: function () {
if (this.props.session.session.user) { if (this.props.session.session.user) {
this.getMessageCount(); var intervalId = setInterval(
var intervalId = setInterval(this.getMessageCount, 120000); // check for new messages every 2 mins. this.props.dispatch(messageCountActions.getCount(this.props.session.session.user.username), 120000)
); // check for new messages every 2 mins.
this.setState({'messageCountIntervalId': intervalId}); this.setState({'messageCountIntervalId': intervalId});
} }
}, },
@ -54,14 +56,15 @@ var Navigation = React.createClass({
'accountNavOpen': false 'accountNavOpen': false
}); });
if (this.props.session.session.user) { if (this.props.session.session.user) {
this.getMessageCount(); var intervalId = setInterval(
var intervalId = setInterval(this.getMessageCount, 120000); this.props.dispatch(messageCountActions.getCount(this.props.session.session.user.username), 120000)
); // check for new messages every 2 mins.
this.setState({'messageCountIntervalId': intervalId}); this.setState({'messageCountIntervalId': intervalId});
} else { } else {
// clear message count check, and set to default id. // clear message count check, and set to default id.
clearInterval(this.state.messageCountIntervalId); clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0));
this.setState({ this.setState({
'unreadMessageCount': 0,
'messageCountIntervalId': -1 'messageCountIntervalId': -1
}); });
} }
@ -71,8 +74,8 @@ var Navigation = React.createClass({
// clear message interval if it exists // clear message interval if it exists
if (this.state.messageCountIntervalId != -1) { if (this.state.messageCountIntervalId != -1) {
clearInterval(this.state.messageCountIntervalId); clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0));
this.setState({ this.setState({
'unreadMessageCount': 0,
'messageCountIntervalId': -1 'messageCountIntervalId': -1
}); });
} }
@ -81,18 +84,6 @@ var Navigation = React.createClass({
if (!this.props.session.session.user) return; if (!this.props.session.session.user) return;
return '/users/' + this.props.session.session.user.username + '/'; 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) { handleJoinClick: function (e) {
e.preventDefault(); e.preventDefault();
this.setState({'registrationOpen': true}); this.setState({'registrationOpen': true});
@ -179,7 +170,7 @@ var Navigation = React.createClass({
}); });
var messageClasses = classNames({ var messageClasses = classNames({
'message-count': true, 'message-count': true,
'show': this.state.unreadMessageCount > 0 'show': this.props.unreadMessageCount > 0
}); });
var dropdownClasses = classNames({ var dropdownClasses = classNames({
'user-info': true, 'user-info': true,
@ -230,7 +221,7 @@ var Navigation = React.createClass({
href="/messages/" href="/messages/"
title={formatMessage({id: 'general.messages'})}> title={formatMessage({id: 'general.messages'})}>
<span className={messageClasses}>{this.state.unreadMessageCount}</span> <span className={messageClasses}>{this.props.unreadMessageCount}</span>
<FormattedMessage id="general.messages" /> <FormattedMessage id="general.messages" />
</a> </a>
</li>, </li>,
@ -343,6 +334,7 @@ var mapStateToProps = function (state) {
return { return {
session: state.session, session: state.session,
permissions: state.permissions, permissions: state.permissions,
unreadMessageCount: state.messageCount.messageCount,
searchTerm: state.navigation searchTerm: state.navigation
}; };
}; };

View file

@ -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.as className={classes}>
<FlexRow className="mod-social-message">
<div className="social-message-content">
{this.props.children}
</div>
<span className="social-message-date">
<FormattedRelative value={new Date(this.props.datetime)} />
</span>
</FlexRow>
</this.props.as>
);
}
});
module.exports = SocialMessage;

View file

@ -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%;
}
}

View file

@ -42,18 +42,19 @@ module.exports = function (opts, callback) {
var apiRequest = function (opts) { var apiRequest = function (opts) {
if (opts.host !== '') { if (opts.host !== '') {
// For IE < 10, we must use XDR for cross-domain requests. XDR does not support if ('withCredentials' in new XMLHttpRequest()) {
// custom headers. opts.useXDR = false;
defaults(opts, {useXDR: true}); } else {
if (opts.useXDR) { // For IE < 10, we must use XDR for cross-domain requests. XDR does not support
// custom headers.
opts.useXDR = true;
delete opts.headers; delete opts.headers;
} if (opts.authentication) {
if (opts.authentication) { var authenticationParams = ['x-token=' + opts.authentication];
var authenticationParams = ['x-token=' + opts.authentication]; var parts = opts.uri.split('?');
var parts = opts.uri.split('?'); var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&');
var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&'); opts.uri = parts[0] + '?' + qs;
opts.uri = parts[0] + '?' + qs; }
} }
} }
xhr(opts, function (err, res, body) { xhr(opts, function (err, res, body) {

View file

@ -12,12 +12,7 @@ var reducer = require('../redux/reducer.js');
require('../main.scss'); require('../main.scss');
var store = redux.createStore( var render = function (jsx, element, reducers) {
reducer,
redux.applyMiddleware(thunk)
);
var render = function (jsx, element) {
// Get locale and messages from global namespace (see "init.js") // Get locale and messages from global namespace (see "init.js")
var locale = window._locale || 'en'; var locale = window._locale || 'en';
var messages = {}; var messages = {};
@ -33,6 +28,12 @@ var render = function (jsx, element) {
messages = window._messages[locale]; messages = window._messages[locale];
} }
var allReducers = reducer(reducers);
var store = redux.createStore(
allReducers,
redux.applyMiddleware(thunk)
);
// Render view component // Render view component
ReactDOM.render( ReactDOM.render(
<StoreProvider store={store}> <StoreProvider store={store}>

View file

@ -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));
});
};
};

245
src/redux/messages.js Normal file
View file

@ -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));
});
};
};

View file

@ -1,17 +1,24 @@
var combineReducers = require('redux').combineReducers; var combineReducers = require('redux').combineReducers;
var scheduleReducer = require('./conference-schedule.js').scheduleReducer; var messageCountReducer = require('./message-count.js').messageCountReducer;
var detailsReducer = require('./conference-details.js').detailsReducer;
var permissionsReducer = require('./permissions.js').permissionsReducer; var permissionsReducer = require('./permissions.js').permissionsReducer;
var sessionReducer = require('./session.js').sessionReducer; var sessionReducer = require('./session.js').sessionReducer;
var navigationReducer = require('./navigation.js').navigationReducer;
var appReducer = combineReducers({ /**
session: sessionReducer, * Returns a combined reducer to be used for a page in `render.jsx`.
permissions: permissionsReducer, * The reducers used globally are applied here - session and permissions
conferenceSchedule: scheduleReducer, * - and any reducers specific to the page should be passed into
conferenceDetails: detailsReducer, * `render()` as an object (which will then be passed to the function
navigation: navigationReducer * below).
}); * @param {Object} opts key/value where the key is the name of the
* redux state, value is the reducer function.
module.exports = appReducer; * @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
}));
};

View file

@ -2,6 +2,7 @@ var keyMirror = require('keymirror');
var defaults = require('lodash.defaults'); var defaults = require('lodash.defaults');
var api = require('../lib/api'); var api = require('../lib/api');
var messageCountActions = require('./message-count.js');
var permissionsActions = require('./permissions.js'); var permissionsActions = require('./permissions.js');
var Types = keyMirror({ var Types = keyMirror({
@ -96,6 +97,7 @@ module.exports.refreshSession = function () {
// get the permissions from the updated session // get the permissions from the updated session
dispatch(permissionsActions.storePermissions(body.permissions)); dispatch(permissionsActions.storePermissions(body.permissions));
dispatch(messageCountActions.getCount(body.user.username));
return; return;
} }
}); });

View file

@ -141,6 +141,13 @@
"view": "jobs/moderator/moderator", "view": "jobs/moderator/moderator",
"title": "Community Moderator" "title": "Community Moderator"
}, },
{
"name": "messages",
"pattern": "^/messages/?$",
"routeAlias": "/messages",
"view": "messages/container",
"title": "Messages"
},
{ {
"name": "microworld-art", "name": "microworld-art",
"pattern": "^/microworlds/art", "pattern": "^/microworlds/art",

View file

@ -91,4 +91,8 @@ var mapStateToProps = function (state) {
var ConnectedDetails = connect(mapStateToProps)(ConferenceDetails); var ConnectedDetails = connect(mapStateToProps)(ConferenceDetails);
render(<Page><ConnectedDetails /></Page>, document.getElementById('app')); render(
<Page><ConnectedDetails /></Page>,
document.getElementById('app'),
{conferenceDetails: detailsActions.detailsReducer}
);

View file

@ -128,4 +128,8 @@ var mapStateToProps = function (state) {
var ConnectedSchedule = connect(mapStateToProps)(ConferenceSchedule); var ConnectedSchedule = connect(mapStateToProps)(ConferenceSchedule);
render(<Page><ConnectedSchedule /></Page>, document.getElementById('app')); render(
<Page><ConnectedSchedule /></Page>,
document.getElementById('app'),
{conferenceSchedule: scheduleActions.scheduleReducer}
);

View file

@ -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(
<MessagesPresentation
sessionStatus={this.props.sessionStatus}
user={this.props.user}
messages={messages}
adminMessages={this.props.adminMessages}
scratcherInvite={this.props.invite}
numNewMessages={this.props.numNewMessages}
handleFilterClick={this.handleFilterClick}
handleAdminDismiss={this.handleMessageDismiss}
loadMore={loadMore}
loadMoreMethod={this.handleLoadMoreMessages}
requestStatus={this.props.requestStatus}
/>
);
}
});
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(
<Page><ConnectedMessages /></Page>,
document.getElementById('app'),
{messages: messageActions.messagesReducer}
);

View file

@ -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"
}

View file

@ -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 (
<li className="admin-message">
<FlexRow className="admin-message-header">
<span className="admin-message-date">
<FormattedDate value={new Date(this.props.datetimeCreated)} />
</span>
<Button
className="mod-admin-message-dismiss"
onClick={this.props.onDismiss.bind(this, 'notification', this.props.id)}
>
<img
className="mod-admin-message-icon"
src="/svgs/modal/close-x.svg"
alt="close-icon"
/>
</Button>
</FlexRow>
<p
className="admin-message-content"
dangerouslySetInnerHTML={{__html: this.props.message}}
/>
</li>
);
}
});
module.exports = AdminMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.datetimePromoted}
>
<FormattedMessage
id='messages.becomeManagerText'
values={{
username: <a
href={actorUri}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
studio: <a href={studioUri}>{this.props.studioTitle}</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = BecomeManagerMessage;

View file

@ -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 <FormattedMessage
id='messages.studioCommentReply'
values={{
profileLink: <a
href={actorLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
commentLink: <a href={commentLink}>{this.props.objectTitle}</a>
}}
/>;
}
} 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 <FormattedMessage
id='messages.commentReply'
values={{
profileLink: <a
href={actorLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
commentLink: <a href={profileLink}>{linkText}</a>
}}
/>;
} else {
// is a profile comment and not a reply, must be own profile
linkText = this.props.intl.formatMessage({
id: 'messages.profileSelf'
});
return <FormattedMessage
id='messages.profileComment'
values={{
profileLink: <a
href={actorLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
commentLink: <a href={profileLink}>{linkText}</a>
}}
/>;
}
} 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 <FormattedMessage
id='messages.commentReply'
values={{
profileLink: <a
href={actorLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
commentLink: <a href={projectLink}>{this.props.objectTitle}</a>
}}
/>;
} else {
return <FormattedMessage
id='messages.projectComment'
values={{
profileLink: <a
href={actorLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
commentLink: <a href={projectLink}>{this.props.objectTitle}</a>
}}
/>;
}
}
},
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 (
<SocialMessage
className={classes}
datetime={this.props.commentDateTime}
>
<p className="comment-message-info">{messageText}</p>
<FlexRow className="mod-comment-message">
<img
className="comment-message-info-img"
src={commentorAvatar}
alt={commentorAvatarAlt}
/>
<Comment
comment={this.props.commentText}
/>
</FlexRow>
</SocialMessage>
);
}
}));
module.exports = CommentMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.datetimePromoted}
>
<FormattedMessage
id='messages.curatorInviteText'
values={{
actorLink: <a
href={actorLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
studioLink: <a href={studioLink}>{this.props.studioTitle}</a>,
tabLink: <a href={studioLink}>{tabText}</a>
}}
/>
</SocialMessage>
);
}
}));
module.exports = CuratorInviteMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.favoriteDateTime}
>
<FormattedMessage
id='messages.favoriteText'
values={{
profileLink: <a
href={profileLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
projectLink: <a href={projectLink}>{this.props.projectTitle}</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = FavoriteProjectMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.followDateTime}
>
<FormattedMessage
id='messages.followText'
values={{
profileLink: <a
href={profileLink}
className="social-messages-profile-link"
>
{this.props.followerUsername}
</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = FollowUserMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.datetimeCreated}
>
<FormattedMessage
id='messages.forumPostText'
values={{
topicLink: <a href={topicLink}>{this.props.topicTitle}</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = ForumPostMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.loveDateTime}
>
<FormattedMessage
id='messages.loveText'
values={{
profileLink: <a
href={profileLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
projectLink: <a href={projectLink}>{this.props.projectTitle}</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = LoveProjectMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.remixDate}
>
<FormattedMessage
id='messages.remixText'
values={{
profileLink: <a
href={profileLink}
className="social-messages-profile-link"
>
{this.props.actorUsername}
</a>,
projectLink: <a href={projectLink}>{this.props.projectTitle}</a>,
remixedProjectLink: <a href={remixedProjectLink}>{this.props.parentTitle}</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = RemixProjectMessage;

View file

@ -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 (
<li className="admin-message">
<FlexRow className="admin-message-header">
<span className="admin-message-date">
<FormattedDate value={new Date(this.props.datetimeCreated)} />
</span>
<Button
className="mod-scratcher-invite-dismiss"
onClick={this.props.onDismiss.bind(this, 'invite', this.props.id)}
>
<img
className="mod-scratcher-invite-icon"
src="/svgs/modal/close-x.svg"
alt="close-icon"
/>
</Button>
</FlexRow>
<p className="admin-message-content">
<FormattedMessage
id='messages.scratcherInvite'
values={{
learnMore: <a href={learnMoreLink}>{learnMoreMessage}</a>
}}
/>
</p>
</li>
);
}
}));
module.exports = AdminMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.datetimeCreated}
>
<FormattedMessage
id='messages.studioActivityText'
values={{
studioLink: <a href={studioLink}>{this.props.studioTitle}</a>
}}
/>
</SocialMessage>
);
}
});
module.exports = StudioActivityMessage;

View file

@ -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 (
<SocialMessage
className={classes}
datetime={this.props.datetimeJoined}
>
<FormattedMessage
id='messages.userJoinText'
values={{
exploreLink: <a href="/explore">{exploreText}</a>,
makeProjectLink: <a href="/projects/editor/?tip_bar=getStarted">{projectText}</a>
}}
/>
</SocialMessage>
);
}
}));
module.exports = UserJoinMessage;

View file

@ -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;
}
}

View file

@ -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 <FollowUserMessage
key={key}
className={className}
followerUsername={message.actor_username}
followDateTime={message.datetime_created}
/>;
case 'loveproject':
return <LoveProjectMessage
key={key}
className={className}
actorUsername={message.actor_username}
projectId={message.project_id}
projectTitle={message.title}
loveDateTime={message.datetime_created}
/>;
case 'favoriteproject':
return <FavoriteProjectMessage
key={key}
className={className}
actorUsername={message.actor_username}
projectId={message.project_id}
projectTitle={message.project_title}
favoriteDateTime={message.datetime_created}
/>;
case 'addcomment':
return <CommentMessage
key={key}
className={className}
actorUsername={message.actor_username}
actorId={message.actor_id}
objectType={message.comment_type}
objectId={message.comment_obj_id}
commentId={message.comment_id}
commentText={message.comment_fragment}
commentDateTime={message.datetime_created}
objectTitle={message.comment_obj_title}
commentee={message.commentee}
/>;
case 'curatorinvite':
return <CuratorInviteMessage
key={key}
className={className}
actorUsername={message.actor_username}
studioId={message.gallery_id}
studioTitle={message.title}
datetimePromoted={message.datetime_created}
/>;
case 'remixproject':
return <RemixProjectMessage
key={key}
className={className}
actorUsername={message.actor_username}
projectId={message.project_id}
projectTitle={message.title}
parentId={message.parent_id}
parentTitle={message.parent_title}
remixDate={message.datetime_created}
/>;
case 'studioactivity':
return <StudioActivityMessage
key={key}
className={className}
studioId={message.gallery_id}
studioTitle={message.title}
datetimeCreated={message.datetime_created}
/>;
case 'forumpost':
return <ForumPostMessage
key={key}
className={className}
actorUsername={message.actor_username}
topicId={message.topic_id}
topicTitle={message.topic_title}
datetimeCreated={message.datetime_created}
/>;
case 'becomeownerstudio':
return <BecomeManagerMessage
key={key}
className={className}
actorUsername={message.actor_username}
studioId={message.gallery_id}
studioTitle={message.gallery_title}
datetimePromoted={message.datetime_created}
/>;
case 'userjoin':
return <UserJoinMessage
key={key}
className={className}
datetimeJoined={message.datetime_created}
/>;
}
},
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 <Button
onClick={this.props.loadMoreMethod}
className="messages-social-loadmore white"
key="load-more"
>
<FormattedMessage id='general.loadMore' />
</Button>;
}
return null;
},
render: function () {
if (this.props.loadStatus === messageStatuses.MESSAGES_ERROR) {
return (
<section className="messages-social">
<div className="messages-social-title">
<h4>
<FormattedMessage id='messages.messageTitle' />
</h4>
</div>
<p><FormattedMessage id='messages.requestError' /></p>
</section>
);
}
return (
<section className="messages-social">
{this.props.messages.length > 0 ? [
<div className="messages-social-title" key="messages-social-title">
<h4>
<FormattedMessage id='messages.messageTitle' />
<span className="messages-social-title-unread">
<FormattedNumber value={this.props.numNewMessages} />
</span>
</h4>
</div>,
<ul className="messages-social-list" key="messages-social-list">
{this.renderSocialMessages(this.props.messages, (this.props.numNewMessages - 1))}
</ul>,
this.renderLoadMore(this.props.loadMore)
] : []}
</section>
);
}
});
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 (
<div className="messages">
<TitleBanner className="mod-messages">
<FlexRow className="inner mod-messages-title">
<h1 className="title-banner-h1 mod-messages">
<FormattedMessage id='messages.messageTitle' />
</h1>
<div className="messages-title-filter">
<Form>
<Select
label={this.props.intl.formatMessage({id: 'messages.filterBy'})}
name="messages.filter"
onChange={this.props.handleFilterClick}
options={[
{
label: this.props.intl.formatMessage({id: 'messages.activityAll'}),
value: ''
},
{
label: this.props.intl.formatMessage({id: 'messages.activityComments'}),
value: 'comments'
},
{
label: this.props.intl.formatMessage({id: 'messages.activityProjects'}),
value: 'projects'
},
{
label: this.props.intl.formatMessage({id: 'messages.activityStudios'}),
value: 'studios'
},
{
label: this.props.intl.formatMessage({id: 'messages.activityForums'}),
value: 'forums'
}
]}
/>
</Form>
</div>
</FlexRow>
</TitleBanner>
<div className="messages-details inner">
{this.props.adminMessages.length > 0 || Object.keys(this.props.scratcherInvite).length > 0 ? [
<section className="messages-admin">
<div className="messages-admin-title">
<h4>
<FormattedMessage id='messages.scratchTeamTitle' />
<span className="messages-social-title-unread">
<FormattedNumber value={adminMessageLength} />
</span>
</h4>
</div>
<ul className="messages-admin-list">
{Object.keys(this.props.scratcherInvite).length > 0 ? [
<ScratcherInvite
id={this.props.scratcherInvite.id}
username={this.props.user.username}
datetimeCreated={this.props.scratcherInvite.datetime_created}
onDismiss={this.props.handleAdminDismiss}
/>
] : []}
{this.props.adminMessages.map(function (item) {
return <AdminMessage
key={'adminmessage' + item.id}
id={item.id}
message={item.message}
datetimeCreated={item.datetime_created}
onDismiss={this.props.handleAdminDismiss.bind('notification', item.id)}
/>;
}.bind(this))}
</ul>
</section>
] : []}
{this.props.requestStatus.admin === messageStatuses.ADMIN_ERROR ? [
<section className="messages-admin">
<h4>
<FormattedMessage id='messages.scratchTeamTitle' />
</h4>
<p><FormattedMessage id='messages.requestError' /></p>
</section>
] : []}
<SocialMessagesList
loadStatus={this.props.requestStatus.messages}
messages={this.props.messages}
numNewMessages={this.props.numNewMessages}
loadMore={this.props.loadMore}
loadMoreMethod={this.props.loadMoreMethod}
/>
</div>
</div>
);
}
}));
module.exports = MessagesPresentation;

View file

@ -135,4 +135,8 @@ var mapStateToProps = function (state) {
var ConnectedSearch = connect(mapStateToProps)(Search); var ConnectedSearch = connect(mapStateToProps)(Search);
render(<Page><ConnectedSearch /></Page>, document.getElementById('app')); render(
<Page><ConnectedSearch /></Page>,
document.getElementById('app'),
{navigation: navigationActions.navigationReducer}
);

View file

@ -38,9 +38,9 @@ var Splash = injectIntl(React.createClass({
if (this.props.user != prevProps.user) { if (this.props.user != prevProps.user) {
if (this.props.user.username) { if (this.props.user.username) {
this.getActivity(this.props.user.username); this.getActivity(this.props.user.username);
this.getSharedByFollowing(this.props.user.token); this.getSharedByFollowing(this.props.user.username, this.props.user.token);
this.getInStudiosFollowing(this.props.user.token); this.getInStudiosFollowing(this.props.user.username, this.props.user.token);
this.getLovedByFollowing(this.props.user.token); this.getLovedByFollowing(this.props.user.username, this.props.user.token);
this.getNews(); this.getNews();
} else { } else {
this.setState({sharedByFollowing: []}); this.setState({sharedByFollowing: []});
@ -61,9 +61,9 @@ var Splash = injectIntl(React.createClass({
this.getFeaturedGlobal(); this.getFeaturedGlobal();
if (this.props.user.username) { if (this.props.user.username) {
this.getActivity(this.props.user.username); this.getActivity(this.props.user.username);
this.getSharedByFollowing(this.props.user.token); this.getSharedByFollowing(this.props.user.username, this.props.user.token);
this.getInStudiosFollowing(this.props.user.token); this.getInStudiosFollowing(this.props.user.username, this.props.user.token);
this.getLovedByFollowing(this.props.user.token); this.getLovedByFollowing(this.props.user.username, this.props.user.token);
this.getNews(); this.getNews();
} else { } else {
this.getProjectCount(); this.getProjectCount();
@ -85,27 +85,27 @@ var Splash = injectIntl(React.createClass({
if (!err) return this.setState({featuredGlobal: body}); if (!err) return this.setState({featuredGlobal: body});
}.bind(this)); }.bind(this));
}, },
getSharedByFollowing: function (token) { getSharedByFollowing: function (username, token) {
api({ api({
uri: '/projects/following/users', uri: '/users/' + username + '/following/users/projects',
authentication: token authentication: token
}, function (err, body) { }, function (err, body) {
if (!body) return log.error('No response body'); if (!body) return log.error('No response body');
if (!err) return this.setState({sharedByFollowing: body}); if (!err) return this.setState({sharedByFollowing: body});
}.bind(this)); }.bind(this));
}, },
getInStudiosFollowing: function (token) { getInStudiosFollowing: function (username, token) {
api({ api({
uri: '/projects/following/studios', uri: '/users/' + username + '/following/studios/projects',
authentication: token authentication: token
}, function (err, body) { }, function (err, body) {
if (!body) return log.error('No response body'); if (!body) return log.error('No response body');
if (!err) return this.setState({inStudiosFollowing: body}); if (!err) return this.setState({inStudiosFollowing: body});
}.bind(this)); }.bind(this));
}, },
getLovedByFollowing: function (token) { getLovedByFollowing: function (username, token) {
api({ api({
uri: '/projects/following/loves', uri: '/users/' + username + '/following/users/loves',
authentication: token authentication: token
}, function (err, body) { }, function (err, body) {
if (!body) return log.error('No response body'); if (!body) return log.error('No response body');

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

BIN
static/images/emoji/cat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
static/images/emoji/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB