Merge branch 'master' into config-embed

This commit is contained in:
Belén Curcio
2017-05-03 16:52:30 -03:00
committed by GitHub
26 changed files with 580 additions and 302 deletions
+1
View File
@@ -19,5 +19,6 @@ plugins/*
!plugins/coral-plugin-facebook-auth !plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-respect !plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic !plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
**/node_modules/* **/node_modules/*
@@ -20,7 +20,6 @@ const fm = new IntrospectionFragmentMatcher({
name: 'Response', name: 'Response',
possibleTypes: [ possibleTypes: [
{name: 'CreateCommentResponse'}, {name: 'CreateCommentResponse'},
{name: 'CreateLikeResponse'},
{name: 'CreateFlagResponse'}, {name: 'CreateFlagResponse'},
{name: 'CreateDontAgreeResponse'}, {name: 'CreateDontAgreeResponse'},
{name: 'DeleteActionResponse'}, {name: 'DeleteActionResponse'},
@@ -38,7 +37,6 @@ const fm = new IntrospectionFragmentMatcher({
name: 'Action', name: 'Action',
possibleTypes: [ possibleTypes: [
{name: 'FlagAction'}, {name: 'FlagAction'},
{name: 'LikeAction'},
{name: 'DontAgreeAction'} {name: 'DontAgreeAction'}
], ],
}, },
@@ -47,7 +45,6 @@ const fm = new IntrospectionFragmentMatcher({
name: 'ActionSummary', name: 'ActionSummary',
possibleTypes: [ possibleTypes: [
{name: 'FlagActionSummary'}, {name: 'FlagActionSummary'},
{name: 'LikeActionSummary'},
{name: 'DontAgreeActionSummary'} {name: 'DontAgreeActionSummary'}
], ],
}, },
@@ -57,7 +54,6 @@ const fm = new IntrospectionFragmentMatcher({
possibleTypes: [ possibleTypes: [
{name: 'DefaultAssetActionSummary'}, {name: 'DefaultAssetActionSummary'},
{name: 'FlagAssetActionSummary'}, {name: 'FlagAssetActionSummary'},
{name: 'LikeAssetActionSummary'}
] ]
} }
], ],
@@ -1,11 +1,3 @@
// this component will
// render its children
// render a like button
// render a permalink button
// render a reply button
// render a flag button
// translate things?
import React, {PropTypes} from 'react'; import React, {PropTypes} from 'react';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
@@ -16,25 +8,33 @@ import Content from 'coral-plugin-commentcontent/CommentContent';
import PubDate from 'coral-plugin-pubdate/PubDate'; import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment'; import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton'; import {
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton'; BestButton,
IfUserCanModifyBest,
BEST_TAG,
commentIsBest,
BestIndicator
} from 'coral-plugin-best/BestButton';
import Slot from 'coral-framework/components/Slot'; import Slot from 'coral-framework/components/Slot';
import LoadMore from './LoadMore'; import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu'; import {TopRightMenu} from './TopRightMenu';
import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils'; import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
import styles from './Comment.css'; import styles from './Comment.css';
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ; const isStaff = tags => !tags.every(t => t.name !== 'STAFF');
// hold actions links (e.g. Like, Reply) along the comment footer // hold actions links (e.g. Reply) along the comment footer
const ActionButton = ({children}) => { const ActionButton = ({children}) => {
return <span className="comment__action-button comment__action-button--nowrap">{ children }</span>; return (
<span className="comment__action-button comment__action-button--nowrap">
{children}
</span>
);
}; };
class Comment extends React.Component { class Comment extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {replyBoxVisible: false}; this.state = {replyBoxVisible: false};
@@ -49,7 +49,6 @@ class Comment extends React.Component {
setActiveReplyBox: PropTypes.func.isRequired, setActiveReplyBox: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired, showSignInDialog: PropTypes.func.isRequired,
postFlag: PropTypes.func.isRequired, postFlag: PropTypes.func.isRequired,
postLike: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired, deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string, parentId: PropTypes.string,
highlighted: PropTypes.string, highlighted: PropTypes.string,
@@ -80,7 +79,8 @@ class Comment extends React.Component {
PropTypes.shape({ PropTypes.shape({
body: PropTypes.string.isRequired, body: PropTypes.string.isRequired,
id: PropTypes.string.isRequired id: PropTypes.string.isRequired
})), })
),
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired name: PropTypes.string.isRequired
@@ -97,10 +97,10 @@ class Comment extends React.Component {
removeCommentTag: React.PropTypes.func, removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user // dispatch action to ignore another user
ignoreUser: React.PropTypes.func, ignoreUser: React.PropTypes.func
} };
render () { render() {
const { const {
comment, comment,
parentId, parentId,
@@ -110,7 +110,6 @@ class Comment extends React.Component {
postItem, postItem,
addNotification, addNotification,
showSignInDialog, showSignInDialog,
postLike,
highlighted, highlighted,
postFlag, postFlag,
postDontAgree, postDontAgree,
@@ -124,12 +123,14 @@ class Comment extends React.Component {
disableReply, disableReply,
commentIsIgnored, commentIsIgnored,
maxCharCount, maxCharCount,
charCountEnable, charCountEnable
} = this.props; } = this.props;
const likeSummary = getActionSummary('LikeActionSummary', comment);
const flagSummary = getActionSummary('FlagActionSummary', comment); const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary('DontAgreeActionSummary', comment); const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
comment
);
let myFlag = null; let myFlag = null;
if (iPerformedThisAction('FlagActionSummary', comment)) { if (iPerformedThisAction('FlagActionSummary', comment)) {
myFlag = flagSummary.find(s => s.current_user); myFlag = flagSummary.find(s => s.current_user);
@@ -137,46 +138,59 @@ class Comment extends React.Component {
myFlag = dontAgreeSummary.find(s => s.current_user); myFlag = dontAgreeSummary.find(s => s.current_user);
} }
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`; let commentClass = parentId
? `reply ${styles.Reply}`
: `comment ${styles.Comment}`;
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : ''; commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async function (...args) { const notifyOnError = (fn, errorToMessage) =>
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;} async function(...args) {
try { if (typeof errorToMessage !== 'function') {
return await fn(...args); errorToMessage = error => error.message;
} catch (error) { }
addNotification('error', errorToMessage(error)); try {
throw error; return await fn(...args);
} } catch (error) {
}; addNotification('error', errorToMessage(error));
throw error;
}
};
const addBestTag = notifyOnError(() => addCommentTag({ const addBestTag = notifyOnError(
id: comment.id, () =>
tag: BEST_TAG, addCommentTag({
}), () => 'Failed to tag comment as best'); id: comment.id,
tag: BEST_TAG
}),
() => 'Failed to tag comment as best'
);
const removeBestTag = notifyOnError(() => removeCommentTag({ const removeBestTag = notifyOnError(
id: comment.id, () =>
tag: BEST_TAG, removeCommentTag({
}), () => 'Failed to remove best comment tag'); id: comment.id,
tag: BEST_TAG
}),
() => 'Failed to remove best comment tag'
);
return ( return (
<div <div
className={commentClass} className={commentClass}
id={`c_${comment.id}`} id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}> style={{marginLeft: depth * 30}}
>
<hr aria-hidden={true} /> <hr aria-hidden={true} />
<div className={highlighted === comment.id ? 'highlighted-comment' : ''}> <div
<AuthorName className={highlighted === comment.id ? 'highlighted-comment' : ''}
author={comment.user}/> >
{ isStaff(comment.tags) <AuthorName author={comment.user} />
? <TagLabel>Staff</TagLabel> {isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
: null }
{ commentIsBest(comment) {commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel> ? <TagLabel><BestIndicator /></TagLabel>
: null } : null}
<PubDate created_at={comment.created_at} /> <PubDate created_at={comment.created_at} />
<Slot <Slot
fill="commentInfoBar" fill="commentInfoBar"
@@ -186,47 +200,43 @@ class Comment extends React.Component {
commentId={comment.id} commentId={comment.id}
inline inline
/> />
{ (currentUser && (comment.user.id !== currentUser.id)) {currentUser && comment.user.id !== currentUser.id
? <span className={styles.topRightMenu}> ? <span className={styles.topRightMenu}>
<TopRightMenu <TopRightMenu
comment={comment} comment={comment}
ignoreUser={ignoreUser} ignoreUser={ignoreUser}
addNotification={addNotification} /> addNotification={addNotification}
/>
</span> </span>
: null : null}
}
<Content body={comment.body} /> <Content body={comment.body} />
<Slot fill="commentContent" /> <Slot fill="commentContent" />
<div className="commentActionsLeft comment__action-container"> <div className="commentActionsLeft comment__action-container">
<Slot fill="commentReactions" inline /> <Slot
<ActionButton> fill="commentReactions"
{/* TODO implmement iPerformedThisAction for the like */} data={this.props.data}
<LikeButton root={this.props.root}
totalLikes={getTotalActionCount('LikeActionSummary', comment)} comment={comment}
like={likeSummary[0]} commentId={comment.id}
id={comment.id} inline
postLike={postLike} />
deleteAction={deleteAction} {!disableReply &&
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</ActionButton>
{
!disableReply &&
<ActionButton> <ActionButton>
<ReplyButton <ReplyButton
onClick={() => setActiveReplyBox(comment.id)} onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id} parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id} currentUserId={currentUser && currentUser.id}
banned={false} /> banned={false}
</ActionButton> />
} </ActionButton>}
<ActionButton> <ActionButton>
<IfUserCanModifyBest user={currentUser}> <IfUserCanModifyBest user={currentUser}>
<BestButton <BestButton
isBest={commentIsBest(comment)} isBest={commentIsBest(comment)}
addBest={addBestTag} addBest={addBestTag}
removeBest={removeBestTag} /> removeBest={removeBestTag}
/>
</IfUserCanModifyBest> </IfUserCanModifyBest>
</ActionButton> </ActionButton>
<Slot <Slot
@@ -252,12 +262,12 @@ class Comment extends React.Component {
postDontAgree={postDontAgree} postDontAgree={postDontAgree}
deleteAction={deleteAction} deleteAction={deleteAction}
showSignInDialog={showSignInDialog} showSignInDialog={showSignInDialog}
currentUser={currentUser} /> currentUser={currentUser}
/>
</ActionButton> </ActionButton>
</div> </div>
</div> </div>
{ {activeReplyBox === comment.id
activeReplyBox === comment.id
? <ReplyBox ? <ReplyBox
commentPostedHandler={() => { commentPostedHandler={() => {
setActiveReplyBox(''); setActiveReplyBox('');
@@ -269,11 +279,10 @@ class Comment extends React.Component {
addNotification={addNotification} addNotification={addNotification}
authorId={currentUser.id} authorId={currentUser.id}
postItem={postItem} postItem={postItem}
assetId={asset.id} /> assetId={asset.id}
: null />
} : null}
{ {comment.replies &&
comment.replies &&
comment.replies.map(reply => { comment.replies.map(reply => {
return commentIsIgnored(reply) return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} /> ? <IgnoredCommentTombstone key={reply.id} />
@@ -290,7 +299,6 @@ class Comment extends React.Component {
asset={asset} asset={asset}
highlighted={highlighted} highlighted={highlighted}
currentUser={currentUser} currentUser={currentUser}
postLike={postLike}
postFlag={postFlag} postFlag={postFlag}
deleteAction={deleteAction} deleteAction={deleteAction}
addCommentTag={addCommentTag} addCommentTag={addCommentTag}
@@ -301,12 +309,11 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog} showSignInDialog={showSignInDialog}
reactKey={reply.id} reactKey={reply.id}
key={reply.id} key={reply.id}
comment={reply} />; comment={reply}
}) />;
} })}
{ {comment.replies &&
comment.replies && <div className="coral-load-more-replies">
<div className='coral-load-more-replies'>
<LoadMore <LoadMore
assetId={asset.id} assetId={asset.id}
comments={comment.replies} comments={comment.replies}
@@ -314,9 +321,9 @@ class Comment extends React.Component {
topLevel={false} topLevel={false}
replyCount={comment.replyCount} replyCount={comment.replyCount}
moreComments={comment.replyCount > comment.replies.length} moreComments={comment.replyCount > comment.replies.length}
loadMore={loadMore}/> loadMore={loadMore}
</div> />
} </div>}
</div> </div>
); );
} }
@@ -29,7 +29,6 @@ class Stream extends React.Component {
postItem, postItem,
addNotification, addNotification,
postFlag, postFlag,
postLike,
postDontAgree, postDontAgree,
loadMore, loadMore,
deleteAction, deleteAction,
@@ -127,7 +126,6 @@ class Stream extends React.Component {
asset={asset} asset={asset}
currentUser={user} currentUser={user}
highlighted={comment.id} highlighted={comment.id}
postLike={this.props.postLike}
postFlag={this.props.postFlag} postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree} postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadMore} loadMore={this.props.loadMore}
@@ -165,7 +163,6 @@ class Stream extends React.Component {
postItem={postItem} postItem={postItem}
asset={asset} asset={asset}
currentUser={user} currentUser={user}
postLike={postLike}
postFlag={postFlag} postFlag={postFlag}
postDontAgree={postDontAgree} postDontAgree={postDontAgree}
addCommentTag={addCommentTag} addCommentTag={addCommentTag}
@@ -6,7 +6,7 @@ import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream'; import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; import {postComment, postFlag, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework'; import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user'; import {editName} from 'coral-framework/actions/user';
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream'; import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
@@ -217,7 +217,6 @@ const mapStateToProps = state => ({
auth: state.auth.toJS(), auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache, commentCountCache: state.stream.commentCountCache,
activeReplyBox: state.stream.activeReplyBox, activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId, commentId: state.stream.commentId,
assetId: state.stream.assetId, assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl, assetUrl: state.stream.assetUrl,
@@ -239,7 +238,6 @@ export default compose(
connect(mapStateToProps, mapDispatchToProps), connect(mapStateToProps, mapDispatchToProps),
postComment, postComment,
postFlag, postFlag,
postLike,
postDontAgree, postDontAgree,
addCommentTag, addCommentTag,
removeCommentTag, removeCommentTag,
@@ -1,7 +1,6 @@
import {graphql} from 'react-apollo'; import {graphql} from 'react-apollo';
import POST_COMMENT from './postComment.graphql'; import POST_COMMENT from './postComment.graphql';
import POST_FLAG from './postFlag.graphql'; import POST_FLAG from './postFlag.graphql';
import POST_LIKE from './postLike.graphql';
import POST_DONT_AGREE from './postDontAgree.graphql'; import POST_DONT_AGREE from './postDontAgree.graphql';
import DELETE_ACTION from './deleteAction.graphql'; import DELETE_ACTION from './deleteAction.graphql';
import ADD_COMMENT_TAG from './addCommentTag.graphql'; import ADD_COMMENT_TAG from './addCommentTag.graphql';
@@ -83,17 +82,6 @@ export const postComment = graphql(POST_COMMENT, {
}), }),
}); });
export const postLike = graphql(POST_LIKE, {
props: ({mutate}) => ({
postLike: (like) => {
return mutate({
variables: {
like
}
});
}}),
});
export const postFlag = graphql(POST_FLAG, { export const postFlag = graphql(POST_FLAG, {
props: ({mutate}) => ({ props: ({mutate}) => ({
postFlag: (flag) => { postFlag: (flag) => {
@@ -1,10 +0,0 @@
mutation CreateLike ($like: CreateLikeInput!) {
createLike(like:$like) {
like {
id
}
errors {
translation_key
}
}
}
-71
View File
@@ -1,71 +0,0 @@
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-likes';
class LikeButton extends Component {
static propTypes = {
like: PropTypes.shape({
current: PropTypes.object,
count: PropTypes.number
}),
id: PropTypes.string,
postLike: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
currentUser: PropTypes.shape({
banned: PropTypes.boolean
}),
}
state = {
localPost: null, // Set to the ID of an action if one is posted
localDelete: false // Set to true is the user deletes an action, unless localPost is already set.
}
render() {
const {like, id, postLike, deleteAction, showSignInDialog, currentUser} = this.props;
let {totalLikes: count} = this.props;
const {localPost, localDelete} = this.state;
const liked = (like && like.current_user && !localDelete) || localPost;
if (localPost) {count += 1;}
if (localDelete) {count -= 1;}
const onLikeClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (currentUser.banned) {
return;
}
if (!liked) { // this comment has not yet been liked by this user.
this.setState({localPost: 'temp'});
postLike({
item_id: id,
item_type: 'COMMENTS'
}).then(({data}) => {
this.setState({localPost: data.createLike.like.id});
});
} else {
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
deleteAction(localPost || like.current_user.id);
}
};
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked ? 'likedButton' : ''}`}>
<span className={`${name}-button-text`}>{lang.t(liked ? 'liked' : 'like')}</span>
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{count > 0 && count}</span>
</button>
</div>;
}
}
export default LikeButton;
const lang = new I18n(translations);
-2
View File
@@ -5,8 +5,6 @@ const Action = {
return 'DontAgreeAction'; return 'DontAgreeAction';
case 'FLAG': case 'FLAG':
return 'FlagAction'; return 'FlagAction';
case 'LIKE':
return 'LikeAction';
} }
}, },
-2
View File
@@ -3,8 +3,6 @@ const ActionSummary = {
switch (action_type) { switch (action_type) {
case 'FLAG': case 'FLAG':
return 'FlagActionSummary'; return 'FlagActionSummary';
case 'LIKE':
return 'LikeActionSummary';
case 'DONTAGREE': case 'DONTAGREE':
return 'DontAgreeActionSummary'; return 'DontAgreeActionSummary';
} }
-2
View File
@@ -12,7 +12,6 @@ const FlagAction = require('./flag_action');
const DontAgreeAction = require('./dont_agree_action'); const DontAgreeAction = require('./dont_agree_action');
const DontAgreeActionSummary = require('./dont_agree_action_summary'); const DontAgreeActionSummary = require('./dont_agree_action_summary');
const GenericUserError = require('./generic_user_error'); const GenericUserError = require('./generic_user_error');
const LikeAction = require('./like_action');
const RootMutation = require('./root_mutation'); const RootMutation = require('./root_mutation');
const RootQuery = require('./root_query'); const RootQuery = require('./root_query');
const Settings = require('./settings'); const Settings = require('./settings');
@@ -36,7 +35,6 @@ let resolvers = {
DontAgreeAction, DontAgreeAction,
DontAgreeActionSummary, DontAgreeActionSummary,
GenericUserError, GenericUserError,
LikeAction,
RootMutation, RootMutation,
RootQuery, RootQuery,
Settings, Settings,
-5
View File
@@ -1,5 +0,0 @@
const LikeAction = {
};
module.exports = LikeAction;
-3
View File
@@ -5,9 +5,6 @@ const RootMutation = {
createComment(_, {comment}, {mutators: {Comment}}) { createComment(_, {comment}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.create(comment)); return wrapResponse('comment')(Comment.create(comment));
}, },
createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
},
createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) { createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) {
return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}})); return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}));
}, },
-62
View File
@@ -103,9 +103,6 @@ enum COMMENT_STATUS {
# The types of action there are as enum's. # The types of action there are as enum's.
enum ACTION_TYPE { enum ACTION_TYPE {
# Represents a LikeAction.
LIKE
# Represents a FlagAction. # Represents a FlagAction.
FLAG FLAG
@@ -295,41 +292,6 @@ type FlagAssetActionSummary implements AssetActionSummary {
actionableItemCount: Int actionableItemCount: Int
} }
# A summary of counts related to all the Likes on an Asset.
type LikeAssetActionSummary implements AssetActionSummary {
# Number of likes associated with actionable types on this this Asset.
actionCount: Int
# Number of unique actionable types that are referenced by the likes.
actionableItemCount: Int
}
# LikeAction is used by users who "like" a specific entity.
type LikeAction implements Action {
# The ID of the action.
id: ID!
# The author of the action.
user: User
# The time when the Action was updated.
updated_at: Date
# The time when the Action was created.
created_at: Date
}
# LikeActionSummary is counts the amount of "likes" that a specific entity has.
type LikeActionSummary implements ActionSummary {
# The count of likes against the parent entity.
count: Int!
current_user: LikeAction
}
# A FLAG action that contains flag metadata. # A FLAG action that contains flag metadata.
type FlagAction implements Action { type FlagAction implements Action {
@@ -541,9 +503,6 @@ enum USER_STATUS {
# Metrics for the assets. # Metrics for the assets.
enum ASSET_METRICS_SORT { enum ASSET_METRICS_SORT {
# Represents a LikeAction.
LIKE
# Represents a FlagAction. # Represents a FlagAction.
FLAG FLAG
@@ -629,15 +588,6 @@ enum ACTION_ITEM_TYPE {
USERS USERS
} }
input CreateLikeInput {
# The item's id for which we are to create a like.
item_id: ID!
# The type of the item for which we are to create the like.
item_type: ACTION_ITEM_TYPE!
}
enum TAG_TYPE { enum TAG_TYPE {
STAFF STAFF
} }
@@ -658,15 +608,6 @@ input CreateCommentInput {
} }
type CreateLikeResponse implements Response {
# The like that was created.
like: LikeAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
}
input CreateFlagInput { input CreateFlagInput {
# The item's id for which we are to create a flag. # The item's id for which we are to create a flag.
@@ -786,9 +727,6 @@ type RootMutation {
# Creates a comment on the asset. # Creates a comment on the asset.
createComment(comment: CreateCommentInput!): CreateCommentResponse createComment(comment: CreateCommentInput!): CreateCommentResponse
# Creates a like on an entity.
createLike(like: CreateLikeInput!): CreateLikeResponse
# Creates a flag on an entity. # Creates a flag on an entity.
createFlag(flag: CreateFlagInput!): CreateFlagResponse createFlag(flag: CreateFlagInput!): CreateFlagResponse
+14
View File
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -0,0 +1,6 @@
import React from 'react';
import cn from 'classnames';
export default ({className}) => (
<i className={cn('fa', 'fa-handshake-o', className)} aria-hidden="true"/>
);
@@ -0,0 +1,89 @@
import React, { Component } from 'react';
import styles from './style.css';
import Icon from './Icon';
import { I18n } from 'coral-framework';
import cn from 'classnames';
import translations from '../translations.json';
import { getMyActionSummary, getTotalActionCount } from 'coral-framework/utils';
const lang = new I18n(translations);
const name = 'coral-plugin-like';
class LikeButton extends Component {
handleClick = () => {
const { postLike, showSignInDialog, deleteAction } = this.props;
const { root: { me }, comment } = this.props;
const myLikeActionSummary = getMyActionSummary(
'LikeActionSummary',
comment
);
// If the current user does not exist, trigger sign in dialog.
if (!me) {
showSignInDialog();
return;
}
// If the current user is banned, do nothing.
if (me.status === 'BANNED') {
return;
}
if (myLikeActionSummary) {
deleteAction(myLikeActionSummary.current_user.id, comment.id);
} else {
postLike({
item_id: comment.id,
item_type: 'COMMENTS'
});
}
};
render() {
const { comment } = this.props;
if (!comment) {
return null;
}
const myLike = getMyActionSummary('LikeActionSummary', comment);
let count = getTotalActionCount('LikeActionSummary', comment);
return (
<div className={cn(styles.like, `${name}-container`)}>
<button
className={cn(
styles.button,
{ [styles.liked]: myLike },
`${name}-button`
)}
onClick={this.handleClick}
>
<span className={`${name}-button-text`}>
{lang.t(myLike ? 'liked' : 'like')}
</span>
<i
className={cn(
styles.icon,
'material-icons',
{ [styles.liked]: myLike },
`${name}-icon`
)}
aria-hidden={true}
>
thumb_up
</i>
<span className={`${name}-count`}>{count > 0 && count}</span>
</button>
</div>
);
}
}
LikeButton.propTypes = {
data: React.PropTypes.object.isRequired
};
export default LikeButton;
@@ -0,0 +1,30 @@
.like {
display: inline-block;
}
.button {
color: #2a2a2a;
margin: 5px 10px 5px 0px;
background: none;
padding: 0px;
border: none;
font-size: inherit;
&:hover {
color: #767676;
cursor: pointer;
}
&.liked {
color: rgb(0,134,227);
&:hover {
color: rgb(0,134,227);
cursor: pointer;
}
}
}
.icon {
padding: 0 5px;
}
@@ -0,0 +1,185 @@
import get from 'lodash/get';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql, graphql } from 'react-apollo';
import LikeButton from '../components/LikeButton';
import withFragments from 'coral-framework/hocs/withFragments';
import { showSignInDialog } from 'coral-framework/actions/auth';
const isLikeAction = a => a.__typename === 'LikeActionSummary';
const COMMENT_FRAGMENT = gql`
fragment LikeButton_updateFragment on Comment {
action_summaries {
... on LikeActionSummary {
count
current_user {
id
}
}
}
}
`;
const withDeleteAction = graphql(
gql`
mutation deleteAction($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
}
}
}
`,
{
props: ({ mutate }) => ({
deleteAction: (id, commentId) => {
return mutate({
variables: { id },
optimisticResponse: {
deleteAction: {
__typename: 'DeleteActionResponse',
errors: null
}
},
update: proxy => {
const fragmentId = `Comment_${commentId}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId
});
// Check whether we liked this comment.
const idx = data.action_summaries.findIndex(isLikeAction);
if (
idx < 0 ||
get(data.action_summaries[idx], 'current_user.id') !== id
) {
return;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count - 1,
current_user: null
};
// Write our data back to the cache.
proxy.writeFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId,
data
});
}
});
}
})
}
);
const withPostLike = graphql(
gql`
mutation createLike($like: CreateLikeInput!) {
createLike(like: $like) {
like {
id
}
errors {
translation_key
}
}
}
`,
{
props: ({ mutate }) => ({
postLike: like => {
return mutate({
variables: { like },
optimisticResponse: {
createLike: {
__typename: 'CreateLikeResponse',
errors: null,
like: {
__typename: 'LikeAction',
id: 'pending'
}
}
},
update: (proxy, mutationResult) => {
const fragmentId = `Comment_${like.item_id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId
});
// Add our comment from the mutation to the end.
let idx = data.action_summaries.findIndex(isLikeAction);
// Check whether we already liked this comment.
if (idx >= 0 && data.action_summaries[idx].current_user) {
return;
}
if (idx < 0) {
// Add initial action when it doesn't exist.
data.action_summaries.push({
__typename: 'LikeActionSummary',
count: 0,
current_user: null
});
idx = data.action_summaries.length - 1;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count + 1,
current_user: mutationResult.data.createLike.like
};
// Write our data back to the cache.
proxy.writeFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId,
data
});
}
});
}
})
}
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog }, dispatch);
const enhance = compose(
withFragments({
root: gql`
fragment LikeButton_root on RootQuery {
me {
status
}
}
`,
comment: gql`
fragment LikeButton_comment on Comment {
action_summaries {
... on LikeActionSummary {
count
current_user {
id
}
}
}
}`
}),
connect(null, mapDispatchToProps),
withDeleteAction,
withPostLike
);
export default enhance(LikeButton);
@@ -0,0 +1,7 @@
import LikeButton from './containers/LikeButton';
export default {
slots: {
commentReactions: [LikeButton]
}
};
+36
View File
@@ -0,0 +1,36 @@
const {readFileSync} = require('fs');
const path = require('path');
const wrapResponse = require('../../graph/helpers/response');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
resolvers: {
RootMutation: {
createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
}
}
},
hooks: {
Action: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'LIKE':
return 'LikeAction';
}
}
}
},
ActionSummary: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'LIKE':
return 'LikeActionSummary';
}
}
}
}
}
};
@@ -0,0 +1,70 @@
enum ACTION_TYPE {
# Represents a Like.
LIKE
}
enum ASSET_METRICS_SORT {
# Represents a LikeAction.
LIKE
}
input CreateLikeInput {
# The item's id for which we are to create a like.
item_id: ID!
# The type of the item for which we are to create the like.
item_type: ACTION_ITEM_TYPE!
}
# LikeAction is used by users who "like" a specific entity.
type LikeAction implements Action {
# The ID of the action.
id: ID!
# The author of the action.
user: User
# The time when the Action was updated.
updated_at: Date
# The time when the Action was created.
created_at: Date
}
type LikeActionSummary implements ActionSummary {
# The count of actions with this group.
count: Int
# The current user's action.
current_user: LikeAction
}
# A summary of counts related to all the Likes on an Asset.
type LikeAssetActionSummary implements AssetActionSummary {
# Number of likes associated with actionable types on this this Asset.
actionCount: Int
# Number of unique actionable types that are referenced by the likes.
actionableItemCount: Int
}
type CreateLikeResponse implements Response {
# The like that was created.
like: LikeAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
}
type RootMutation {
# Creates a like on an entity.
createLike(like: CreateLikeInput!): CreateLikeResponse
}
+7 -2
View File
@@ -3,11 +3,16 @@ const path = require('path');
const wrapResponse = require('../../graph/helpers/response'); const wrapResponse = require('../../graph/helpers/response');
module.exports = { module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), typeDefs: readFileSync(
path.join(__dirname, 'server/typeDefs.graphql'),
'utf8'
),
resolvers: { resolvers: {
RootMutation: { RootMutation: {
createRespect(_, {respect: {item_id, item_type}}, {mutators: {Action}}) { createRespect(_, {respect: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('respect')(Action.create({item_id, item_type, action_type: 'RESPECT'})); return wrapResponse('respect')(
Action.create({item_id, item_type, action_type: 'RESPECT'})
);
} }
} }
}, },
+13 -30
View File
@@ -15,9 +15,6 @@ describe('graph.loaders.Metrics', () => {
describe('#Comments', () => { describe('#Comments', () => {
const query = ` const query = `
query CommentMetrics($from: Date!, $to: Date!) { query CommentMetrics($from: Date!, $to: Date!) {
liked: commentMetrics(from: $from, to: $to, sort: LIKE) {
id
}
flagged: commentMetrics(from: $from, to: $to, sort: FLAG) { flagged: commentMetrics(from: $from, to: $to, sort: FLAG) {
id id
} }
@@ -33,26 +30,21 @@ describe('graph.loaders.Metrics', () => {
])); ]));
[ [
{liked: 0, flagged: 0, actions: []}, {flagged: 0, actions: []},
{liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}]}, {flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
{liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]}, {flagged: 1, actions: [
{liked: 1, flagged: 1, actions: [
{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}, {action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'},
{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}
]}, ]},
{liked: 3, flagged: 1, actions: [ {flagged: 1, actions: [
{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'},
{action_type: 'LIKE', item_id: '2', item_type: 'COMMENTS'},
{action_type: 'LIKE', item_id: '3', item_type: 'COMMENTS'},
{action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'} {action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'}
]} ]}
].forEach(({liked, flagged, actions}) => { ].forEach(({flagged, actions}) => {
describe(`with actions=${actions.length}`, () => { describe(`with actions=${actions.length}`, () => {
beforeEach(() => ActionModel.create(actions)); beforeEach(() => ActionModel.create(actions));
it(`returns the correct amount of metrics liked=${liked} flagged=${flagged}`, () => { it(`returns the correct amount of metrics flagged=${flagged}`, () => {
const context = new Context({user: new UserModel({roles: ['ADMIN']})}); const context = new Context({user: new UserModel({roles: ['ADMIN']})});
return graphql(schema, query, {}, context, { return graphql(schema, query, {}, context, {
@@ -60,8 +52,8 @@ describe('graph.loaders.Metrics', () => {
to: (new Date()).setMinutes((new Date()).getMinutes() + 5) to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
}) })
.then(({data, errors}) => { .then(({data, errors}) => {
console.log(errors);
expect(errors).to.be.undefined; expect(errors).to.be.undefined;
expect(data.liked).to.have.length(liked);
expect(data.flagged).to.have.length(flagged); expect(data.flagged).to.have.length(flagged);
}); });
}); });
@@ -88,9 +80,6 @@ describe('graph.loaders.Metrics', () => {
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) { assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
...metrics ...metrics
} }
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
...metrics
}
} }
`; `;
@@ -109,26 +98,21 @@ describe('graph.loaders.Metrics', () => {
])); ]));
[ [
{liked: 0, flagged: 0, actions: []}, {flagged: 0, actions: []},
{liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}]}, {flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
{liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]}, {flagged: 1, actions: [
{liked: 1, flagged: 1, actions: [
{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}, {action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'},
{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}
]}, ]},
{liked: 1, flagged: 1, actions: [ {flagged: 1, actions: [
{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'},
{action_type: 'LIKE', item_id: 'c2', item_type: 'COMMENTS'},
{action_type: 'LIKE', item_id: 'c3', item_type: 'COMMENTS'},
{action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'} {action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'}
]} ]}
].forEach(({liked, flagged, actions}) => { ].forEach(({flagged, actions}) => {
describe(`with actions=${actions.length}`, () => { describe(`with actions=${actions.length}`, () => {
beforeEach(() => ActionModel.create(actions)); beforeEach(() => ActionModel.create(actions));
it(`returns the correct amount of metrics liked=${liked} flagged=${flagged}`, () => { it(`returns the correct amount of metrics flagged=${flagged}`, () => {
const context = new Context({user: new UserModel({roles: ['ADMIN']})}); const context = new Context({user: new UserModel({roles: ['ADMIN']})});
return graphql(schema, query, {}, context, { return graphql(schema, query, {}, context, {
@@ -137,7 +121,6 @@ describe('graph.loaders.Metrics', () => {
}) })
.then(({data, errors}) => { .then(({data, errors}) => {
expect(errors).to.be.undefined; expect(errors).to.be.undefined;
expect(data.assetsByLike).to.have.length(liked);
expect(data.assetsByFlag).to.have.length(flagged); expect(data.assetsByFlag).to.have.length(flagged);
}); });
}); });