Merge with master

This commit is contained in:
Dan Zajdband
2016-12-05 16:39:00 -05:00
49 changed files with 907 additions and 384 deletions
+8 -7
View File
@@ -12,26 +12,27 @@ const linkify = new Linkify();
// Render a single comment for the list
export default props => {
const links = linkify.getMatches(props.comment.get('body'));
const {comment, author} = props;
const links = linkify.getMatches(comment.get('body'));
return (
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<i className={`material-icons ${styles.avatar}`}>person</i>
<span>{props.comment.get('name') || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{props.comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
<span>{author.get('displayName') || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
</div>
<div>
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={styles.actions}>
{props.actions.map((action, i) => canShowAction(action, props.comment) ? (
{props.actions.map((action, i) => canShowAction(action, comment) ? (
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
cStyle={action}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment.get('id'))}
/>
) : null)}
</div>
@@ -40,7 +41,7 @@ export default props => {
<div className={styles.itemBody}>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
{props.comment.get('body')}
{comment.get('body')}
</Linkify>
</span>
</div>
@@ -112,13 +112,15 @@ export default class CommentList extends React.Component {
}
render () {
const {singleView, commentIds, comments, hideActive, key} = this.props;
const {singleView, commentIds, comments, users, hideActive, key} = this.props;
const {active} = this.state;
return (
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
{commentIds.map((commentId, index) => (
<Comment comment={comments.get(commentId)}
{commentIds.map((commentId, index) => {
const comment = comments.get(commentId);
return <Comment comment={comment}
author={users.get(comment.get('author_id'))}
ref={el => { if (el && commentId === active) { this._active = el; } }}
key={index}
index={index}
@@ -126,8 +128,8 @@ export default class CommentList extends React.Component {
actions={this.props.actions}
actionsMap={actions}
isActive={commentId === active}
hideActive={hideActive} />
)).toArray()}
hideActive={hideActive} />;
}).toArray()}
</ul>
);
}
@@ -40,7 +40,7 @@ class CommentStream extends React.Component {
}
// Render the comment box along with the CommentList
render ({comments}, {snackbar, snackbarMsg}) {
render ({comments, users}, {snackbar, snackbarMsg}) {
return (
<div className={styles.container}>
<CommentBox onSubmit={this.onSubmit} />
@@ -48,6 +48,7 @@ class CommentStream extends React.Component {
singleView={false}
commentIds={comments.get('ids')}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={this.onClickAction}
actions={['flag']}
loading={comments.loading} />
@@ -57,4 +58,4 @@ class CommentStream extends React.Component {
}
}
export default connect(({comments}) => ({comments}))(CommentStream);
export default connect(({comments, users}) => ({comments, users}))(CommentStream);
@@ -61,7 +61,7 @@ class ModerationQueue extends React.Component {
// Render the tabbed lists moderation queues
render () {
const {comments} = this.props;
const {comments, users} = this.props;
const {activeTab, singleView, modalOpen} = this.state;
return (
@@ -82,10 +82,11 @@ class ModerationQueue extends React.Component {
commentIds={
comments.get('ids')
.filter(id => !comments.get('byId')
.get(id)
.get('status'))
.get(id)
.get('status'))
}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['reject', 'approve']}
loading={comments.loading} />
@@ -104,6 +105,7 @@ class ModerationQueue extends React.Component {
.get('status') === 'rejected')
}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['approve']}
loading={comments.loading} />
@@ -117,6 +119,7 @@ class ModerationQueue extends React.Component {
return !data.get('status') && data.get('flagged') === true;
})}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['reject', 'approve']}
loading={comments.loading} />
@@ -129,6 +132,6 @@ class ModerationQueue extends React.Component {
}
}
export default connect(({comments}) => ({comments}))(ModerationQueue);
export default connect(({comments, users}) => ({comments, users}))(ModerationQueue);
const lang = new I18n(translations);
+1 -4
View File
@@ -24,10 +24,7 @@ export default function auth (state = initialState, action) {
.set('isAdmin', action.isAdmin)
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return state
.set('loggedIn', false)
.set('user', null)
.set('isAdmin', false);
return initialState;
default :
return state;
}
+3 -2
View File
@@ -2,6 +2,7 @@ import {combineReducers} from 'redux';
import comments from 'reducers/comments';
import settings from 'reducers/settings';
import community from 'reducers/community';
import users from 'reducers/users';
import auth from 'reducers/auth';
// Combine all reducers into a main one
@@ -9,6 +10,6 @@ export default combineReducers({
settings,
comments,
community,
auth
auth,
users
});
+20
View File
@@ -0,0 +1,20 @@
import {Map, List, fromJS} from 'immutable';
const initialState = Map({
byId: Map(),
ids: List()
});
export default (state = initialState, action) => {
switch (action.type) {
case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
default: return state;
}
};
// Replace the comment list with a new one
const replaceUsers = (action, state) => {
const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
return state.set('byId', users)
.set('ids', List(users.keys()));
};
@@ -15,9 +15,6 @@ export default store => next => action => {
case 'COMMENTS_MODERATION_QUEUE_FETCH':
fetchModerationQueueComments(store);
break;
// case 'COMMENT_STREAM_FETCH':
// fetchCommentStream(store);
// break;
case 'COMMENT_UPDATE':
updateComment(store, action.comment);
break;
@@ -38,12 +35,31 @@ Promise.all([
coralApi('/comments?action_type=flag')
])
.then(([pending, rejected, flagged]) => {
flagged.forEach(comment => comment.flagged = true);
return [...pending, ...rejected, ...flagged];
/* Combine seperate calls into a single object */
let all = {};
all.comments = pending.comments
.concat(rejected.comments)
.concat(flagged.comments.map(comment => {
comment.flagged = true;
return comment;
}));
all.users = pending.users
.concat(rejected.users)
.concat(flagged.users);
all.actions = pending.actions
.concat(rejected.actions)
.concat(flagged.actions);
return all;
})
.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: res}))
.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
.then(all => {
/* Post comments and users to redux store. Actions will be posted when they are needed. */
store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS',
users: all.users});
store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: all.comments});
});
// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
// Update a comment. Now to update a comment we need to send back the whole object
+152 -159
View File
@@ -1,4 +1,7 @@
import React, {Component, PropTypes} from 'react';
import Pym from 'pym.js';
import {connect} from 'react-redux';
import {
itemActions,
Notification,
@@ -6,7 +9,7 @@ import {
authActions,
configActions
} from '../../coral-framework';
import {connect} from 'react-redux';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
import InfoBox from '../../coral-plugin-infobox/InfoBox';
import Content from '../../coral-plugin-commentcontent/CommentContent';
@@ -14,47 +17,22 @@ import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import Pym from 'pym.js';
import FlagButton from '../../coral-plugin-flags/FlagButton';
import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import UserBox from '../../coral-sign-in/components/UserBox';
import {TabBar, Tab, TabContent, Button} from '../../coral-ui';
import {TabBar, Tab, TabContent, Spinner, Button} from '../../coral-ui';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import {Icon} from 'react-mdl';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout} = authActions;
const {logout, showSignInDialog} = authActions;
const {updateOpenStatus} = configActions;
const mapStateToProps = (state) => {
return {
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS()
};
};
const mapDispatchToProps = (dispatch) => ({
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
deleteAction: (item, action, user, itemType) => {
return dispatch(deleteAction(item, action, user, itemType));
},
appendItemArray: (item, property, value, addToFront, itemType) =>
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
logout: () => dispatch(logout()),
updateStatus: status => dispatch(updateOpenStatus(status))
});
class CommentStream extends Component {
constructor (props) {
@@ -87,47 +65,22 @@ class CommentStream extends Component {
componentDidMount () {
// Set up messaging between embedded Iframe an parent component
// Using recommended Pym init code which violates .eslint standards
this.pym = new Pym.Child({polling: 100});
const pym = new Pym.Child({polling: 100});
const path = this.pym.parentUrl.split('#')[0];
this.props.getStream(path || window.location);
this.path = path;
this.pym.sendMessage('childReady');
this.pym.onMessage('DOMContentLoaded', hash => {
// the comment ids can start with numbers, which is invalid for DOM id attributes
const commentId = hash.replace('#', 'c_');
let count = 0;
const interval = setInterval(() => {
if (document.getElementById(commentId)) {
window.clearInterval(interval);
this.pym.scrollParentToChildEl(commentId);
}
if (++count > 100) { // ~10 seconds
// give up waiting for the comments to load.
// it would be weird for the page to jump after that long.
window.clearInterval(interval);
}
}, 100);
});
if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) {
this.props.getStream(pym.parentUrl);
} else {
this.props.getStream(window.location);
}
}
render () {
if (Object.keys(this.props.items).length === 0) {
// Loading mock asset
// Loading mock asset
this.props.postItem({
comments: [],
url: 'http://coralproject.net'
}, 'asset', 'assetTest');
// Loading mock user
//this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
// .then((id) => {
// this.props.setLoggedInUser(id);
// });
}
// TODO: Replace teststream id with id from params
@@ -142,73 +95,76 @@ class CommentStream extends Component {
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
{
rootItem
? <div className="commentStream">
? <div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
<Tab>Settings</Tab>
<Tab>Configure Stream</Tab>
</TabBar>
<TabContent show={activeTab === 0}>
{
status === 'open'
? <div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
author={user}
/>
{!loggedIn && <SignInContainer />}
</div>
: <p>Comments are closed for this thread</p>
}
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
showReply={comment.showReply}/>
<LikeButton
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
{/* Add to the restricted param a boolean if the user is suspended*/}
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
<TabContent show={activeTab === 0}>
{
status === 'open'
? <div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<CommentBox
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
author={user}
/>
{!loggedIn && <SignInContainer />}
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
: <p>Comments are closed for this thread.</p>
}
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
showSignInDialog={this.props.showSignInDialog}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={commentId}
articleURL={this.path}/>
</div>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
@@ -229,46 +185,48 @@ class CommentStream extends Component {
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
<ReplyButton
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
showReply={reply.showReply}/>
</div>;
})
}
@@ -282,15 +240,26 @@ class CommentStream extends Component {
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer/>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.handleSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
<CloseCommentsInfo onClick={this.toggleStatus}
status={status} />
</TabContent>
</RestrictedContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
: 'Loading'
:
<Spinner/>
}
</div>;
}
@@ -315,4 +284,28 @@ const CloseCommentsInfo = ({ status, onClick }) => status === 'open' ? (
</div>
)
const mapStateToProps = state => ({
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS()
});
const mapDispatchToProps = (dispatch) => ({
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
showSignInDialog: () => dispatch(showSignInDialog()),
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
logout: () => dispatch(logout()),
updateStatus: status => dispatch(updateOpenStatus(status))
});
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
+6 -2
View File
@@ -56,12 +56,14 @@ hr {
/* Info Box Styles */
.coral-plugin-infobox-info {
position: fixed;
top: 0;
border: 0;
background: rgb(105,105,105);
color: white;
border-radius: 2px;
width: 100%;
text-align: center;
padding: 10px;
margin-bottom: 10px;
font-weight: bold;
display: block;
}
@@ -84,6 +86,7 @@ hr {
.coral-plugin-commentbox-textarea {
flex: 1;
padding: 5px;
min-height: 100px;
}
.coral-plugin-commentbox-button-container {
@@ -110,6 +113,7 @@ hr {
/* Comment styles */
.comment {
position: relative;
margin-bottom: 10px;
position: relative;
}
+21
View File
@@ -0,0 +1,21 @@
import * as actions from '../constants/user';
import {addNotification} from '../actions/notification';
import coralApi from '../helpers/response';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
const saveBioRequest = () => ({type: actions.SAVE_BIO_REQUEST});
const saveBioSuccess = settings => ({type: actions.SAVE_BIO_SUCCESS, settings});
const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
export const saveBio = (user_id, formData) => dispatch => {
dispatch(saveBioRequest());
coralApi(`/user/${user_id}/bio`, {method: 'PUT', body: formData})
.then(({settings}) => {
dispatch(addNotification('success', lang.t('successBioUpdate')));
dispatch(saveBioSuccess(settings));
})
.catch(error => dispatch(saveBioFailure(error)));
};
@@ -0,0 +1,4 @@
.message {
background: #D8D8D8;
padding: 25px;
}
@@ -0,0 +1,20 @@
import React from 'react';
import styles from './RestrictedContent.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
export default ({children, restricted, message = lang.t('contentNotAvailable'), restrictedComp}) => {
if (restricted) {
return restrictedComp ? restrictedComp : messageBox(message);
} else {
return (
<div>
{children}
</div>
);
}
};
const messageBox = (message) => <div className={styles.message}>{message}</div>;
@@ -0,0 +1,8 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
export default () => (
<span>{lang.t('suspendedAccountMsg')}</span>
);
+3
View File
@@ -0,0 +1,3 @@
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
+8 -3
View File
@@ -13,6 +13,11 @@ const initialState = Map({
successSignUp: false
});
const purge = user => {
const {settings, profiles, ...userData} = user; // eslint-disable-line
return userData;
};
export default function auth (state = initialState, action) {
switch (action.type) {
case actions.SHOW_SIGNIN_DIALOG :
@@ -44,11 +49,11 @@ export default function auth (state = initialState, action) {
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('loggedIn', true)
.set('user', action.user);
.set('user', purge(action.user));
case actions.FETCH_SIGNIN_SUCCESS:
return state
.set('loggedIn', true)
.set('user', action.user);
.set('user', purge(action.user));
case actions.FETCH_SIGNIN_FAILURE:
return state
.set('isLoading', false)
@@ -56,7 +61,7 @@ export default function auth (state = initialState, action) {
.set('user', null);
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return state
.set('user', action.user)
.set('user', purge(action.user))
.set('loggedIn', true);
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return state
+2
View File
@@ -5,6 +5,7 @@ import config from './config';
import items from './items';
import notification from './notification';
import auth from './auth';
import user from './user';
/**
* Expose the combined main reducer
@@ -15,4 +16,5 @@ export default combineReducers({
items,
notification,
auth,
user
});
+36
View File
@@ -0,0 +1,36 @@
import {Map} from 'immutable';
import * as authActions from '../constants/auth';
import * as actions from '../constants/user';
const initialState = Map({
displayName: '',
profiles: [],
settings: {}
});
const purge = user => {
const {_id, created_at, updated_at, __v, roles, ...userData} = user; // eslint-disable-line
return userData;
};
export default function user (state = initialState, action) {
switch (action.type) {
case authActions.CHECK_LOGIN_SUCCESS:
return state.merge(Map(purge(action.user)));
case authActions.CHECK_LOGIN_FAILURE:
return initialState;
case authActions.FETCH_SIGNIN_SUCCESS:
return state.merge(Map(purge(action.user)));
case authActions.FETCH_SIGNIN_FAILURE:
return initialState;
case authActions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return state.merge(Map(purge(action.user)));
case authActions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return initialState;
case actions.SAVE_BIO_SUCCESS:
return state
.set('settings', action.settings);
default :
return state;
}
}
+7 -1
View File
@@ -1,5 +1,8 @@
{
"en": {
"successBioUpdate": "Your Bio has been updated",
"contentNotAvailable": "This content is not available",
"suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
"error": {
"email": "Not a valid E-Mail",
"password": "Password must be at least 8 characters",
@@ -10,6 +13,9 @@
}
},
"es": {
"successBioUpdate": "Tu bio fue actualizada",
"contentNotAvailable": "El contenido no se encuentra disponible",
"suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"error": {
"email": "No es un email válido",
"password": "La contraseña debe tener por lo menos 8 caracteres",
@@ -19,4 +25,4 @@
"emailInUse": "Email address already in use"
}
}
}
}
+40 -6
View File
@@ -1,9 +1,43 @@
import React from 'react';
import React, {Component} from 'react';
import {Tooltip} from 'coral-ui';
const packagename = 'coral-plugin-author-name';
const AuthorName = ({author}) =>
<div className={`${packagename}-text`}>
{author && author.displayName}
</div>;
export default class AuthorName extends Component {
constructor (props) {
super(props);
export default AuthorName;
this.state = {
showTooltip: false
};
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
}
handleMouseOver () {
this.setState({
showTooltip: true
});
}
handleMouseLeave () {
this.setState({
showTooltip: false
});
}
render () {
const {author} = this.props;
const {showTooltip} = this.state;
return (
<div
className={`${packagename}-text`}
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
>
{author && author.displayName}
{ showTooltip && <Tooltip>{author.settings.bio}</Tooltip>}
</div>
);
}
}
+4 -4
View File
@@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import {Button} from 'coral-ui';
const name = 'coral-plugin-commentbox';
@@ -75,12 +76,11 @@ class CommentBox extends Component {
</div>
<div className={`${name}-button-container`}>
{ author && (
<button
className={`${name}-button`}
style={styles && styles.button}
<Button
cStyle='darkGrey'
onClick={this.postComment}>
{lang.t('post')}
</button>
</Button>
)
}
</div>
+2 -1
View File
@@ -4,10 +4,11 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, addNotification, currentUser}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (!flagged) {
+2 -1
View File
@@ -4,10 +4,11 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (!liked) {
+8 -6
View File
@@ -2,15 +2,17 @@ import React from 'react';
import styles from './Bio.css';
import {Button} from '../../coral-ui';
export default () => (
export default ({bio, handleSave, handleInput, handleCancel}) => (
<div className={styles.bio}>
<h1>Bio</h1>
<p>Tell the community about yourself</p>
<textarea />
<div className={styles.actions}>
<Button cStyle='cancel' raised>Cancel</Button>
<Button cStyle='success'>Save Changes</Button>
</div>
<form>
<textarea value={bio} onChange={handleInput} />
<div className={styles.actions}>
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
</div>
</form>
</div>
);
@@ -0,0 +1,15 @@
.message {
padding: 10px 0 20px;
letter-spacing: 0.1px;
font-size: 13px;
line-height: 33px;
}
.message a {
color: black;
font-weight: bold;
cursor: pointer;
margin: 0px;
padding-bottom: 2px;
border-bottom: solid 1px black;
}
@@ -0,0 +1,17 @@
import React from 'react';
import styles from './NotLoggedIn.css';
export default ({showSignInDialog}) => (
<div className={styles.message}>
<div>
<a onClick={showSignInDialog}>Sign In</a> to access Settings
</div>
<div>
From the Settings Page you can
<ul>
<li>See your comment history</li>
<li>Write a bio about yourself to display to the community</li>
</ul>
</div>
</div>
);
@@ -1,10 +1,10 @@
import React from 'react';
import styles from './SettingsHeader.css';
export default () => (
export default ({userData}) => (
<div className={styles.header}>
<h1>Jackson</h1>
<h2>jackson_persona@gmail.com</h2>
<h1>{userData.displayName}</h1>
<h2>{userData.profiles.map(profile => profile.id)}</h2>
</div>
);
@@ -0,0 +1,45 @@
import React, {Component} from 'react';
import Bio from '../components/Bio';
export default class BioContainer extends Component {
constructor (props) {
super(props);
this.state = {
bio: props.bio
};
this.handleSave = this.handleSave.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleInput(e) {
this.setState({
bio: e.target.value
});
}
handleSave (e) {
e.preventDefault();
const {userData, saveBio} = this.props;
const {bio} = this.state;
saveBio(userData.id, {bio});
}
handleCancel () {
this.setState({
bio: this.props.bio
});
}
render () {
return <Bio
bio={this.state.bio}
userData={this.props.userData}
handleSave={this.handleSave}
handleInput={this.handleInput}
handleCancel={this.handleCancel}
/>;
}
}
@@ -1,24 +1,26 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {TabBar, Tab} from '../../coral-ui';
import {saveBio} from 'coral-framework/actions/user';
import Bio from '../components/Bio';
import BioContainer from './BioContainer';
import NotLoggedIn from '../components/NotLoggedIn';
import {TabBar, Tab, TabContent} from '../../coral-ui';
import CommentHistory from '../components/CommentHistory';
import SettingsHeader from '../components/SettingsHeader';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
class SignInContainer extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0
activeTab: 0,
};
this.handleTabChange = this.handleTabChange.bind(this);
}
componentWillMount () {
// Get Bio
// Fetch commentHistory
}
@@ -29,18 +31,22 @@ class SignInContainer extends Component {
}
render() {
//const {embedStream} = this.props;
const {loggedIn, userData, showSignInDialog} = this.props;
const {activeTab} = this.state;
return (
<div>
<SettingsHeader />
<RestrictedContent restricted={!loggedIn} restrictedComp={<NotLoggedIn showSignInDialog={showSignInDialog} />}>
<SettingsHeader {...this.props} />
<TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
<Tab>All Comments (120)</Tab>
<Tab>Profile Settings</Tab>
</TabBar>
{ activeTab === 0 && <CommentHistory {...this.props}/> }
{ activeTab === 1 && <Bio {...this.props}/> }
</div>
<TabContent show={activeTab === 0}>
<CommentHistory {...this.props}/>
</TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
</TabContent>
</RestrictedContent>
);
}
}
@@ -49,10 +55,8 @@ const mapStateToProps = () => ({
});
const mapDispatchToProps = dispatch => ({
getBio: () => dispatch(),
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)),
getHistory: () => dispatch(),
handleSaveChanges: () => dispatch(),
handleCancel: () => dispatch()
});
export default connect(
+5 -2
View File
@@ -1,12 +1,15 @@
import React from 'react';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const UserBox = ({className, user, logout, ...props}) => (
<div
className={`${styles.userBox} ${className}`}
className={`${styles.userBox} ${className ? className : ''}`}
{...props}
>
Signed in as <a>{user.displayName}</a>. Not you? <a onClick={logout}>Sign out</a>
{lang.t('signIn.loggedInAs')} <a>{user.displayName}</a>. {lang.t('signIn.notYou')} <a onClick={logout}>{lang.t('signIn.logout')}</a>
</div>
);
+9 -1
View File
@@ -103,10 +103,18 @@ input.error{
color: #B71C1C;
}
.userBox {
padding: 10px 0 20px;
letter-spacing: 0.1px;
}
.userBox a {
color: #2c69b6;
color: black;
font-weight: bold;
cursor: pointer;
margin: 0px;
padding-bottom: 2px;
border-bottom: solid 1px black;
}
.attention {
@@ -129,12 +129,10 @@ class SignInContainer extends Component {
}
render() {
const {auth, showSignInDialog} = this.props;
const {auth, showSignInDialog, noButton} = this.props;
return (
<div>
<Button onClick={showSignInDialog} full>
Sign in to comment
</Button>
{!noButton && <Button onClick={showSignInDialog} full> Sign in to comment</Button>}
<SignDialog
open={auth.showSignInDialog}
view={auth.view}
+4
View File
@@ -1,6 +1,8 @@
export default {
en: {
'signIn': {
notYou: 'Not you?',
loggedInAs: 'Logged in as',
facebookSignIn: 'Sign in with Facebook',
facebookSignUp: 'Sign up with Facebook',
logout: 'Logout',
@@ -24,6 +26,8 @@ export default {
},
es: {
'signIn': {
notYou: 'No eres tu?',
loggedInAs: 'Entraste como',
facebookSignIn: 'Entrar con Facebook',
facebookSignUp: 'Regístrate con Facebook',
logout: 'Salir',
+10
View File
@@ -67,6 +67,16 @@
background: #4f5c67;
}
.type--darkGrey {
color: white;
background: #696969;
}
.type--darkGrey:hover {
color: white;
background: #696969;
}
.full {
width: 100%;
margin: 0;
+1
View File
@@ -15,6 +15,7 @@
padding: 8px 10px;
margin-right: -1px;
user-select: none;
font-size: 13px;
}
.base li:hover {
+36
View File
@@ -0,0 +1,36 @@
.tooltip {
display: inline-block;
position: absolute;
width: 100%;
border: solid 1px #2376D8;
top: 33px;
left: 0;
box-sizing: border-box;
background: white;
border-radius: 3px;
padding: 20px 10px;
z-index: 3;
/*box-shadow: 1px 1px 4px rgba(0,105,255,.5);*/
}
.tooltip:before{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
left: 2em;
top: -19px;
transform: rotate(180deg);
z-index: 2;
}
.tooltip:after{
content: '';
border: 10px solid transparent;
border-top-color: #2376D8;
position: absolute;
left: 2em;
top: -20px;
transform: rotate(180deg);
z-index: 1;
}
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
import styles from './Tooltip.css';
export default ({children}) => (
<span className={styles.tooltip}>{children}</span>
);
+2
View File
@@ -5,3 +5,5 @@ export {default as TabBar} from './components/TabBar';
export {default as Tab} from './components/Tab';
export {default as TabContent} from './components/TabContent';
export {default as Button} from './components/Button';
export {default as Spinner} from './components/Spinner';
export {default as Tooltip} from './components/Tooltip';