Merge branch 'master' of github.com:coralproject/talk into plugin_examples

This commit is contained in:
Belen Curcio
2017-04-17 14:17:42 -03:00
52 changed files with 1380 additions and 272 deletions
+11
View File
@@ -12,3 +12,14 @@
filter: blur(2px);
pointer-events: none;
}
.topRightMenu {
float: right;
text-align: right;
cursor: pointer;
margin-top: 5px;
}
.topRightMenu > * {
text-align: initial;
}
+45 -23
View File
@@ -20,6 +20,8 @@ import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
import {Slot} from 'coral-framework';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import styles from './Comment.css';
@@ -84,11 +86,17 @@ class Comment extends React.Component {
}).isRequired
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: React.PropTypes.func,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
}
render () {
@@ -111,7 +119,9 @@ class Comment extends React.Component {
deleteAction,
addCommentTag,
removeCommentTag,
ignoreUser,
disableReply,
commentIsIgnored,
} = this.props;
const like = getActionSummary('LikeActionSummary', comment);
@@ -121,10 +131,10 @@ class Comment extends React.Component {
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async () => {
const notifyOnError = (fn, errorToMessage) => async function (...args) {
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
try {
return await fn();
return await fn(...args);
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
@@ -160,6 +170,16 @@ class Comment extends React.Component {
<PubDate created_at={comment.created_at} />
<Slot fill="commentInfoBar" comment={comment} commentId={comment.id} inline/>
{ (currentUser && (comment.user.id !== currentUser.id))
? <span className={styles.topRightMenu}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
: null
}
<Content body={comment.body} />
<div className="commentActionsLeft comment__action-container">
<ActionButton>
@@ -225,27 +245,29 @@ class Comment extends React.Component {
{
comment.replies &&
comment.replies.map(reply => {
return <Comment
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply}
/>;
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
})
}
{
+32 -24
View File
@@ -14,7 +14,7 @@ const {fetchAssetSuccess} = assetActions;
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
@@ -71,6 +71,9 @@ class Embed extends React.Component {
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
}
componentDidMount () {
@@ -155,13 +158,14 @@ class Embed extends React.Component {
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const userBox = <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>;
return (
<div style={expandForLogin}>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.totalCommentCount}/>
</Tab>
<Tab>{lang.t('MY_COMMENTS')}</Tab>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
@@ -174,8 +178,8 @@ class Embed extends React.Component {
this.props.data.refetch();
}}>{lang.t('showAllComments')}</Button>
}
{loggedIn && <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>}
<TabContent show={activeTab === 0}>
{ loggedIn ? userBox : null }
{
openStream
? <div id="commentBox">
@@ -266,10 +270,12 @@ class Embed extends React.Component {
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
ignoreUser={this.props.ignoreUser}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
comments={asset.comments}
ignoredUsers={this.props.userData.ignoredUsers} />
</div>
<LoadMore
topLevel={true}
@@ -279,22 +285,23 @@ class Embed extends React.Component {
loadMore={this.props.loadMore} />
</div>
}
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.showSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.showSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
{ loggedIn ? userBox : null }
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</div>
</div>
);
@@ -304,7 +311,7 @@ class Embed extends React.Component {
const mapStateToProps = state => ({
auth: state.auth.toJS(),
userData: state.user.toJS(),
asset: state.asset.toJS()
asset: state.asset.toJS(),
});
const mapDispatchToProps = dispatch => ({
@@ -317,7 +324,7 @@ const mapDispatchToProps = dispatch => ({
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
viewAllComments: () => dispatch(viewAllComments()),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
dispatch: d => dispatch(d),
});
export default compose(
@@ -328,6 +335,7 @@ export default compose(
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
queryStream,
)(Embed);
@@ -0,0 +1,14 @@
.IgnoreUserWizard {
background-color: #2E343B;
color: white;
padding: 1em;
max-width: 220px;
}
.IgnoreUserWizard header {
font-weight: bold;
}
.IgnoreUserWizard .textAlignRight {
text-align: right;
}
@@ -0,0 +1,66 @@
import React, {PropTypes} from 'react';
import styles from './IgnoreUserWizard.css';
import {Button} from 'coral-ui';
// Guides the user through ignoring another user, including confirming their decision
export class IgnoreUserWizard extends React.Component {
static propTypes = {
// comment on which this menu appears
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
cancel: PropTypes.func.isRequired,
// actually submit the ignore. Provide {id: user id to ignore}
ignoreUser: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
// what step of the wizard is the user on
step: 1
};
this.onClickCancel = this.onClickCancel.bind(this);
}
onClickCancel() {
this.props.cancel();
}
render() {
const {user, ignoreUser} = this.props;
const goToStep = (stepNum) => this.setState({step: stepNum});
const step1 = (
<div>
<header>Ignore User</header>
<p>When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.</p>
<div className={styles.textAlignRight}>
<Button cStyle='cancel' onClick={this.onClickCancel}>Cancel</Button>
<Button onClick={() => goToStep(2)}>Ignore user</Button>
</div>
</div>
);
const onClickIgnoreUser = async () => {
await ignoreUser({id: user.id});
};
const step2Confirmation = (
<div>
<header>Ignore User</header>
<p>Are you sure you want to ignore { user.name }?</p>
<div className={styles.textAlignRight}>
<Button cStyle='cancel' onClick={this.onClickCancel}>Cancel</Button>
<Button onClick={onClickIgnoreUser}>Ignore user</Button>
</div>
</div>
);
const elsForStep = [step1, step2Confirmation];
const {step} = this.state;
const elForThisStep = elsForStep[step - 1];
return (
<div className={styles.IgnoreUserWizard}>
{ elForThisStep }
</div>
);
}
}
@@ -0,0 +1,22 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
// Render in place of a Comment when the author of the comment is ignored
const IgnoredCommentTombstone = () => (
<div>
<hr aria-hidden={true} />
<p style={{
backgroundColor: '#F0F0F0',
textAlign: 'center',
padding: '1em',
color: '#3E4F71',
}}>
{lang.t('commentIsIgnored')}
</p>
</div>
);
export default IgnoredCommentTombstone;
+39 -24
View File
@@ -1,5 +1,6 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
class Stream extends React.Component {
@@ -19,6 +20,12 @@ class Stream extends React.Component {
// dispatch action to remove a tag from a comment
removeCommentTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
// list of user ids that should be rendered as ignored
ignoredUsers: React.PropTypes.arrayOf(React.PropTypes.string)
}
constructor(props) {
@@ -42,35 +49,43 @@ class Stream extends React.Component {
showSignInDialog,
addCommentTag,
removeCommentTag,
pluginProps
pluginProps,
ignoreUser,
ignoredUsers,
} = this.props;
const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id);
return (
<div id='stream'>
{
comments.map(comment =>
<Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
/>
commentIsIgnored(comment)
? <IgnoredCommentTombstone
key={comment.id}
/>
: <Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
/>
)
}
</div>
@@ -0,0 +1,24 @@
.Toggleable:focus {
outline: none;
}
/**
* Up/Down Chevrons for the top right menu
*/
.chevron {
}
.chevron:before {
content: '⌃';
display: inline-block;
position: relative;
top: 0.25em;
}
/* Down Arrow */
.chevron.down:before {
display: inline-block;
position: relative;
transform: rotate(180deg);
top: 0;
/*top: -0.25em;*/
}
@@ -0,0 +1,94 @@
import React, {PropTypes} from 'react';
import classnames from 'classnames';
import {IgnoreUserWizard} from './IgnoreUserWizard';
import styles from './TopRightMenu.css';
// TopRightMenu appears as a dropdown in the top right of the comment.
// when you click the down cehvron, it expands and shows IgnoreUserWizard
// when you click 'cancel' in the wizard, it closes the menu
export class TopRightMenu extends React.Component {
static propTypes = {
// comment on which this menu appears
comment: PropTypes.shape({
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired,
ignoreUser: PropTypes.func,
// show notification to the user (e.g. for errors)
addNotification: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
timesReset: 0
};
}
render() {
const {comment, ignoreUser, addNotification} = this.props;
// timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
const reset = () => this.setState({timesReset: this.state.timesReset + 1});
const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => {
// close menu
reset();
// ignore user
try {
await ignoreUser({id});
} catch (error) {
addNotification('error', 'Failed to ignore user');
throw error;
}
};
return (
<Toggleable key={this.state.timesReset}>
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
<IgnoreUserWizard
user={comment.user}
cancel={reset}
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
/>
</div>
</Toggleable>
);
}
}
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
class Toggleable extends React.Component {
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.close = this.close.bind(this);
this.state = {
isOpen: false
};
}
toggle() {
this.setState({isOpen: ! this.state.isOpen});
}
close() {
this.setState({isOpen: false});
}
render() {
const {children} = this.props;
const {isOpen} = this.state;
return (
// /*onBlur={ this.close } */
<span className={styles.Toggleable} tabIndex="0" >
<span className={styles.toggler}
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
{isOpen ? children : null}
</span>
);
}
}