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
34
src/components/comment/comment.jsx
Normal 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;
|
42
src/components/comment/comment.scss
Normal 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;
|
||||
}
|
33
src/components/emoji-text/emoji-text.jsx
Normal 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;
|
4
src/components/emoji-text/emoji-text.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.emoji {
|
||||
max-width: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
|
@ -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'})}>
|
||||
|
||||
<span className={messageClasses}>{this.state.unreadMessageCount}</span>
|
||||
<span className={messageClasses}>{this.props.unreadMessageCount}</span>
|
||||
<FormattedMessage id="general.messages" />
|
||||
</a>
|
||||
</li>,
|
||||
|
@ -343,6 +334,7 @@ var mapStateToProps = function (state) {
|
|||
return {
|
||||
session: state.session,
|
||||
permissions: state.permissions,
|
||||
unreadMessageCount: state.messageCount.messageCount,
|
||||
searchTerm: state.navigation
|
||||
};
|
||||
};
|
||||
|
|
40
src/components/social-message/social-message.jsx
Normal 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;
|
66
src/components/social-message/social-message.scss
Normal 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%;
|
||||
}
|
||||
}
|
|
@ -42,18 +42,19 @@ module.exports = function (opts, callback) {
|
|||
|
||||
var apiRequest = function (opts) {
|
||||
if (opts.host !== '') {
|
||||
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.
|
||||
defaults(opts, {useXDR: true});
|
||||
if (opts.useXDR) {
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr(opts, function (err, res, body) {
|
||||
|
|
|
@ -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(
|
||||
<StoreProvider store={store}>
|
||||
|
|
70
src/redux/message-count.js
Normal 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
|
@ -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));
|
||||
});
|
||||
};
|
||||
};
|
|
@ -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({
|
||||
/**
|
||||
* 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,
|
||||
conferenceSchedule: scheduleReducer,
|
||||
conferenceDetails: detailsReducer,
|
||||
navigation: navigationReducer
|
||||
});
|
||||
|
||||
module.exports = appReducer;
|
||||
messageCount: messageCountReducer
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -91,4 +91,8 @@ var mapStateToProps = function (state) {
|
|||
|
||||
var ConnectedDetails = connect(mapStateToProps)(ConferenceDetails);
|
||||
|
||||
render(<Page><ConnectedDetails /></Page>, document.getElementById('app'));
|
||||
render(
|
||||
<Page><ConnectedDetails /></Page>,
|
||||
document.getElementById('app'),
|
||||
{conferenceDetails: detailsActions.detailsReducer}
|
||||
);
|
||||
|
|
|
@ -128,4 +128,8 @@ var mapStateToProps = function (state) {
|
|||
|
||||
var ConnectedSchedule = connect(mapStateToProps)(ConferenceSchedule);
|
||||
|
||||
render(<Page><ConnectedSchedule /></Page>, document.getElementById('app'));
|
||||
render(
|
||||
<Page><ConnectedSchedule /></Page>,
|
||||
document.getElementById('app'),
|
||||
{conferenceSchedule: scheduleActions.scheduleReducer}
|
||||
);
|
||||
|
|
168
src/views/messages/container.jsx
Normal 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}
|
||||
);
|
30
src/views/messages/l10n.json
Normal 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"
|
||||
}
|
42
src/views/messages/message-rows/admin-message.jsx
Normal 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;
|
45
src/views/messages/message-rows/become-manager.jsx
Normal 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;
|
161
src/views/messages/message-rows/comment-message.jsx
Normal 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;
|
48
src/views/messages/message-rows/curator-invite.jsx
Normal 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;
|
45
src/views/messages/message-rows/favorite-project.jsx
Normal 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;
|
41
src/views/messages/message-rows/follow-user.jsx
Normal 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;
|
38
src/views/messages/message-rows/forum-topic-post.jsx
Normal 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;
|
45
src/views/messages/message-rows/love-project.jsx
Normal 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;
|
49
src/views/messages/message-rows/remix-project.jsx
Normal 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;
|
50
src/views/messages/message-rows/scratcher-invite.jsx
Normal 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;
|
37
src/views/messages/message-rows/studio-activity.jsx
Normal 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;
|
38
src/views/messages/message-rows/user-join.jsx
Normal 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;
|
127
src/views/messages/messages.scss
Normal 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;
|
||||
}
|
||||
}
|
321
src/views/messages/presentation.jsx
Normal 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;
|
|
@ -135,4 +135,8 @@ var mapStateToProps = function (state) {
|
|||
|
||||
var ConnectedSearch = connect(mapStateToProps)(Search);
|
||||
|
||||
render(<Page><ConnectedSearch /></Page>, document.getElementById('app'));
|
||||
render(
|
||||
<Page><ConnectedSearch /></Page>,
|
||||
document.getElementById('app'),
|
||||
{navigation: navigationActions.navigationReducer}
|
||||
);
|
||||
|
|
|
@ -38,9 +38,9 @@ var Splash = injectIntl(React.createClass({
|
|||
if (this.props.user != prevProps.user) {
|
||||
if (this.props.user.username) {
|
||||
this.getActivity(this.props.user.username);
|
||||
this.getSharedByFollowing(this.props.user.token);
|
||||
this.getInStudiosFollowing(this.props.user.token);
|
||||
this.getLovedByFollowing(this.props.user.token);
|
||||
this.getSharedByFollowing(this.props.user.username, this.props.user.token);
|
||||
this.getInStudiosFollowing(this.props.user.username, this.props.user.token);
|
||||
this.getLovedByFollowing(this.props.user.username, this.props.user.token);
|
||||
this.getNews();
|
||||
} else {
|
||||
this.setState({sharedByFollowing: []});
|
||||
|
@ -61,9 +61,9 @@ var Splash = injectIntl(React.createClass({
|
|||
this.getFeaturedGlobal();
|
||||
if (this.props.user.username) {
|
||||
this.getActivity(this.props.user.username);
|
||||
this.getSharedByFollowing(this.props.user.token);
|
||||
this.getInStudiosFollowing(this.props.user.token);
|
||||
this.getLovedByFollowing(this.props.user.token);
|
||||
this.getSharedByFollowing(this.props.user.username, this.props.user.token);
|
||||
this.getInStudiosFollowing(this.props.user.username, this.props.user.token);
|
||||
this.getLovedByFollowing(this.props.user.username, this.props.user.token);
|
||||
this.getNews();
|
||||
} else {
|
||||
this.getProjectCount();
|
||||
|
@ -85,27 +85,27 @@ var Splash = injectIntl(React.createClass({
|
|||
if (!err) return this.setState({featuredGlobal: body});
|
||||
}.bind(this));
|
||||
},
|
||||
getSharedByFollowing: function (token) {
|
||||
getSharedByFollowing: function (username, token) {
|
||||
api({
|
||||
uri: '/projects/following/users',
|
||||
uri: '/users/' + username + '/following/users/projects',
|
||||
authentication: token
|
||||
}, function (err, body) {
|
||||
if (!body) return log.error('No response body');
|
||||
if (!err) return this.setState({sharedByFollowing: body});
|
||||
}.bind(this));
|
||||
},
|
||||
getInStudiosFollowing: function (token) {
|
||||
getInStudiosFollowing: function (username, token) {
|
||||
api({
|
||||
uri: '/projects/following/studios',
|
||||
uri: '/users/' + username + '/following/studios/projects',
|
||||
authentication: token
|
||||
}, function (err, body) {
|
||||
if (!body) return log.error('No response body');
|
||||
if (!err) return this.setState({inStudiosFollowing: body});
|
||||
}.bind(this));
|
||||
},
|
||||
getLovedByFollowing: function (token) {
|
||||
getLovedByFollowing: function (username, token) {
|
||||
api({
|
||||
uri: '/projects/following/loves',
|
||||
uri: '/users/' + username + '/following/users/loves',
|
||||
authentication: token
|
||||
}, function (err, body) {
|
||||
if (!body) return log.error('No response body');
|
||||
|
|
BIN
static/images/emoji/10mil.png
Normal file
After Width: | Height: | Size: 716 B |
BIN
static/images/emoji/apple.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
static/images/emoji/aww-cat.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
static/images/emoji/binoculars.png
Normal file
After Width: | Height: | Size: 910 B |
BIN
static/images/emoji/broccoli.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/emoji/camera.png
Normal file
After Width: | Height: | Size: 959 B |
BIN
static/images/emoji/candycorn.png
Normal file
After Width: | Height: | Size: 613 B |
BIN
static/images/emoji/cat.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/emoji/compass.png
Normal file
After Width: | Height: | Size: 827 B |
BIN
static/images/emoji/cool-cat.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/images/emoji/cupcake.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
static/images/emoji/eggplant.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/emoji/fav-it-cat.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
static/images/emoji/gobo.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
static/images/emoji/huh-cat.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/images/emoji/lol-cat.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/images/emoji/love-it-cat.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/emoji/map.png
Normal file
After Width: | Height: | Size: 937 B |
BIN
static/images/emoji/meow.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
static/images/emoji/pizza-cat.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/emoji/pizza.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
static/images/emoji/rainbow-cat.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/emoji/suitcase.png
Normal file
After Width: | Height: | Size: 545 B |
BIN
static/images/emoji/sushi.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/emoji/taco.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/emoji/tongue-out-cat.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/emoji/upside-down-cat.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
static/images/emoji/waffle.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
static/images/emoji/wink-cat.png
Normal file
After Width: | Height: | Size: 4.5 KiB |