mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge branch 'master' into i18n-refactor
This commit is contained in:
@@ -19,5 +19,6 @@ plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -3,9 +3,8 @@ import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
|
||||
const ActionButton = ({type = '', status, ...props}) => {
|
||||
const ActionButton = ({type = '', active, ...props}) => {
|
||||
const typeName = type.toLowerCase();
|
||||
const active = ((type === 'REJECT' && status === 'REJECTED') || (type === 'APPROVE' && status === 'ACCEPTED'));
|
||||
let text = menuActionsMap[type].text;
|
||||
|
||||
if (text === 'Approve' && active) {
|
||||
@@ -25,7 +24,7 @@ const ActionButton = ({type = '', status, ...props}) => {
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
status: PropTypes.string
|
||||
active: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
|
||||
@@ -21,7 +21,7 @@ const FlagWidget = ({assets}) => {
|
||||
? assets.map(asset => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
|
||||
flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import key from 'keymaster';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import styles from './components/styles.css';
|
||||
|
||||
import {modQueueQuery} from '../../graphql/queries';
|
||||
import {modQueueQuery, getQueueCounts} from '../../graphql/queries';
|
||||
import {banUser, setCommentStatus} from '../../graphql/mutations';
|
||||
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
@@ -220,6 +220,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
setCommentStatus,
|
||||
getQueueCounts,
|
||||
modQueueQuery,
|
||||
banUser
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -9,51 +9,67 @@ import translations from 'coral-admin/src/translations';
|
||||
import LoadMore from './components/LoadMore';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props}) => {
|
||||
return (
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul style={{paddingLeft: 0}}>
|
||||
{
|
||||
comments.length
|
||||
? comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={i === selectedIndex}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
|
||||
}
|
||||
</ul>
|
||||
<LoadMore
|
||||
comments={comments}
|
||||
loadMore={loadMore}
|
||||
sort={sort}
|
||||
tab={activeTab}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
assetId={props.assetId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class ModerationQueue extends React.Component {
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired
|
||||
};
|
||||
static propTypes = {
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
const {loadMore, comments, commentCount, sort, activeTab: tab, assetId: asset_id} = this.props;
|
||||
|
||||
// if the user just moderated the last (visible) comment
|
||||
// AND there are more comments available on the server,
|
||||
// go ahead and load more comments
|
||||
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
|
||||
loadMore({sort, tab, asset_id});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props} = this.props;
|
||||
|
||||
return (
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul style={{paddingLeft: 0}}>
|
||||
{
|
||||
comments.length
|
||||
? comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={i === selectedIndex}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
|
||||
}
|
||||
</ul>
|
||||
<LoadMore
|
||||
comments={comments}
|
||||
loadMore={loadMore}
|
||||
sort={sort}
|
||||
tab={activeTab}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
assetId={props.assetId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModerationQueue;
|
||||
|
||||
@@ -66,15 +66,17 @@ const Comment = ({actions = [], comment, ...props}) => {
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
acceptComment={() => props.acceptComment({commentId: comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: comment.id})}
|
||||
/>
|
||||
)}
|
||||
{actions.map((action, i) => {
|
||||
const active = (action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return <ActionButton key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() => props.acceptComment({commentId: comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: comment.id})} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentCount.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const CommentCount = props => (
|
||||
<span className={styles.count}>{props.count}</span>
|
||||
);
|
||||
const CommentCount = ({count}) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
if (number / 1e9 > 1) {
|
||||
number = `${(number / 1e9).toFixed(1)}${lang.t('modqueue.billion')}`;
|
||||
} else if (number / 1e6 > 1) {
|
||||
number = `${(number / 1e6).toFixed(1)}${lang.t('modqueue.million')}`;
|
||||
} else if (number / 1e3 > 1) {
|
||||
number = `${(number / 1e3).toFixed(1)}${lang.t('modqueue.thousand')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.count}>{number}</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
|
||||
@@ -7,13 +7,16 @@ const LoadMore = ({comments, loadMore, sort, tab, assetId, showLoadMore}) =>
|
||||
{
|
||||
showLoadMore && <Button
|
||||
className={styles.loadMore}
|
||||
onClick={() =>
|
||||
loadMore({
|
||||
cursor: comments[comments.length - 1].created_at,
|
||||
onClick={() => {
|
||||
const lastComment = comments[comments.length - 1];
|
||||
const cursor = lastComment ? lastComment.created_at : null;
|
||||
return loadMore({
|
||||
cursor,
|
||||
sort,
|
||||
tab,
|
||||
asset_id: assetId
|
||||
})}>
|
||||
});
|
||||
}}>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ fragment metrics on Asset {
|
||||
created_at
|
||||
commentCount
|
||||
action_summaries {
|
||||
type: __typename
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export const suspendUser = graphql(SUSPEND_USER, {
|
||||
})
|
||||
});
|
||||
|
||||
const views = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
|
||||
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
acceptComment: ({commentId}) => {
|
||||
@@ -54,7 +55,9 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
},
|
||||
updateQueries: {
|
||||
ModQueue: (oldData) => {
|
||||
const comment = oldData.all.find(c => c.id === commentId);
|
||||
const comment = views.reduce((comment, view) => {
|
||||
return comment ? comment : oldData[view].find(c => c.id === commentId);
|
||||
}, null);
|
||||
let accepted;
|
||||
let acceptedCount = oldData.acceptedCount;
|
||||
|
||||
@@ -76,10 +79,10 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount,
|
||||
flaggedCount,
|
||||
acceptedCount,
|
||||
rejectedCount,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
@@ -97,7 +100,9 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
},
|
||||
updateQueries: {
|
||||
ModQueue: (oldData) => {
|
||||
const comment = oldData.all.find(c => c.id === commentId);
|
||||
const comment = views.reduce((comment, view) => {
|
||||
return comment ? comment : oldData[view].find(c => c.id === commentId);
|
||||
}, null);
|
||||
let rejected;
|
||||
let rejectedCount = oldData.rejectedCount;
|
||||
|
||||
@@ -119,10 +124,10 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount,
|
||||
flaggedCount,
|
||||
acceptedCount,
|
||||
rejectedCount,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
query Counts ($asset_id: ID) {
|
||||
allCount: commentCount(query: {
|
||||
asset_id: $asset_id
|
||||
})
|
||||
acceptedCount: commentCount(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
rejectedCount: commentCount(query: {
|
||||
statuses: [REJECTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
flaggedCount: commentCount(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
|
||||
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
|
||||
import METRICS from './metricsQuery.graphql';
|
||||
import GET_QUEUE_COUNTS from './getQueueCounts.graphql';
|
||||
|
||||
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
options: ({params: {id = null}}) => {
|
||||
@@ -33,34 +34,34 @@ export const getMetrics = graphql(METRICS, {
|
||||
}
|
||||
});
|
||||
|
||||
export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
|
||||
let statuses;
|
||||
export const loadMore = (fetchMore) => ({limit = 10, cursor, sort, tab, asset_id}) => {
|
||||
let variables = {
|
||||
limit,
|
||||
cursor,
|
||||
sort,
|
||||
asset_id
|
||||
};
|
||||
switch(tab) {
|
||||
case 'all':
|
||||
statuses = null;
|
||||
variables.statuses = null;
|
||||
break;
|
||||
case 'accepted':
|
||||
statuses = ['ACCEPTED'];
|
||||
variables.statuses = ['ACCEPTED'];
|
||||
break;
|
||||
case 'premod':
|
||||
statuses = ['PREMOD'];
|
||||
variables.statuses = ['PREMOD'];
|
||||
break;
|
||||
case 'flagged':
|
||||
statuses = ['NONE', 'PREMOD'];
|
||||
variables.statuses = ['NONE', 'PREMOD'];
|
||||
variables.action_type = 'FLAG';
|
||||
break;
|
||||
case 'rejected':
|
||||
statuses = ['REJECTED'];
|
||||
variables.statuses = ['REJECTED'];
|
||||
break;
|
||||
}
|
||||
return fetchMore({
|
||||
query: MOD_QUEUE_LOAD_MORE,
|
||||
variables: {
|
||||
limit,
|
||||
cursor,
|
||||
sort,
|
||||
statuses,
|
||||
asset_id
|
||||
},
|
||||
variables,
|
||||
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
|
||||
return {
|
||||
...oldData,
|
||||
@@ -93,3 +94,14 @@ export const modQueueResort = (id, fetchMore) => (sort) => {
|
||||
updateQuery: (oldData, {fetchMoreResult:{data}}) => data
|
||||
});
|
||||
};
|
||||
|
||||
export const getQueueCounts = graphql(GET_QUEUE_COUNTS, {
|
||||
options: ({params: {id = null}}) => {
|
||||
return {
|
||||
pollInterval: 5000,
|
||||
variables: {
|
||||
asset_id: id
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort}) {
|
||||
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
|
||||
...commentView
|
||||
action_summaries {
|
||||
count
|
||||
|
||||
@@ -20,7 +20,6 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
name: 'Response',
|
||||
possibleTypes: [
|
||||
{name: 'CreateCommentResponse'},
|
||||
{name: 'CreateLikeResponse'},
|
||||
{name: 'CreateFlagResponse'},
|
||||
{name: 'CreateDontAgreeResponse'},
|
||||
{name: 'DeleteActionResponse'},
|
||||
@@ -37,8 +36,8 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
kind: 'INTERFACE',
|
||||
name: 'Action',
|
||||
possibleTypes: [
|
||||
{name: 'DefaultAction'},
|
||||
{name: 'FlagAction'},
|
||||
{name: 'LikeAction'},
|
||||
{name: 'DontAgreeAction'}
|
||||
],
|
||||
},
|
||||
@@ -46,8 +45,8 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
kind: 'INTERFACE',
|
||||
name: 'ActionSummary',
|
||||
possibleTypes: [
|
||||
{name: 'DefaultActionSummary'},
|
||||
{name: 'FlagActionSummary'},
|
||||
{name: 'LikeActionSummary'},
|
||||
{name: 'DontAgreeActionSummary'}
|
||||
],
|
||||
},
|
||||
@@ -57,7 +56,6 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
possibleTypes: [
|
||||
{name: 'DefaultAssetActionSummary'},
|
||||
{name: 'FlagAssetActionSummary'},
|
||||
{name: 'LikeAssetActionSummary'}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -63,7 +63,10 @@
|
||||
"impersonating": "Impersonating",
|
||||
"offensive": "Offensive",
|
||||
"spam/ads": "Spam/Ads",
|
||||
"other": "Other"
|
||||
"other": "Other",
|
||||
"thousand": "k",
|
||||
"million": "M",
|
||||
"billion": "B"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
@@ -248,7 +251,10 @@
|
||||
"impersonating": "Suplantación",
|
||||
"offensive": "Ofensivo",
|
||||
"spam/ads": "Spam/Propaganda",
|
||||
"other": "Otros"
|
||||
"other": "Otros",
|
||||
"thousand": "m",
|
||||
"million": "M",
|
||||
"billion": "B"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
}
|
||||
|
||||
.apply {
|
||||
position: absolute;
|
||||
top: 38%;
|
||||
transform: translateX(-50%);
|
||||
right: 0;
|
||||
float: right;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.wrapper ul {
|
||||
|
||||
@@ -10,8 +10,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
<form onSubmit={handleApply}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.container}>
|
||||
<h3>{lang.t('configure.title')}</h3>
|
||||
<p>{lang.t('configure.description')}</p>
|
||||
<h3>{lang.t('configureCommentStream.title')}</h3>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.apply}
|
||||
@@ -19,6 +18,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
cStyle={changed ? 'green' : 'darkGrey'} >
|
||||
{lang.t('configure.apply')}
|
||||
</Button>
|
||||
<p>{lang.t('configureCommentStream.description')}</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
@@ -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 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 {ReplyBox, ReplyButton} from 'coral-plugin-replies';
|
||||
import FlagComment from 'coral-plugin-flags/FlagComment';
|
||||
import LikeButton from 'coral-plugin-likes/LikeButton';
|
||||
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
|
||||
import {
|
||||
BestButton,
|
||||
IfUserCanModifyBest,
|
||||
BEST_TAG,
|
||||
commentIsBest,
|
||||
BestIndicator
|
||||
} from 'coral-plugin-best/BestButton';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import LoadMore from './LoadMore';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import {TopRightMenu} from './TopRightMenu';
|
||||
import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils';
|
||||
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
|
||||
|
||||
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}) => {
|
||||
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 {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {replyBoxVisible: false};
|
||||
@@ -49,7 +49,6 @@ class Comment extends React.Component {
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
postFlag: PropTypes.func.isRequired,
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
highlighted: PropTypes.string,
|
||||
@@ -80,7 +79,8 @@ class Comment extends React.Component {
|
||||
PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired
|
||||
})),
|
||||
})
|
||||
),
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
@@ -97,10 +97,10 @@ class Comment extends React.Component {
|
||||
removeCommentTag: React.PropTypes.func,
|
||||
|
||||
// dispatch action to ignore another user
|
||||
ignoreUser: React.PropTypes.func,
|
||||
}
|
||||
ignoreUser: React.PropTypes.func
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
comment,
|
||||
parentId,
|
||||
@@ -110,7 +110,6 @@ class Comment extends React.Component {
|
||||
postItem,
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postLike,
|
||||
highlighted,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
@@ -124,12 +123,14 @@ class Comment extends React.Component {
|
||||
disableReply,
|
||||
commentIsIgnored,
|
||||
maxCharCount,
|
||||
charCountEnable,
|
||||
charCountEnable
|
||||
} = this.props;
|
||||
|
||||
const likeSummary = getActionSummary('LikeActionSummary', comment);
|
||||
const flagSummary = getActionSummary('FlagActionSummary', comment);
|
||||
const dontAgreeSummary = getActionSummary('DontAgreeActionSummary', comment);
|
||||
const dontAgreeSummary = getActionSummary(
|
||||
'DontAgreeActionSummary',
|
||||
comment
|
||||
);
|
||||
let myFlag = null;
|
||||
if (iPerformedThisAction('FlagActionSummary', comment)) {
|
||||
myFlag = flagSummary.find(s => s.current_user);
|
||||
@@ -137,46 +138,59 @@ class Comment extends React.Component {
|
||||
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}` : '';
|
||||
|
||||
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
|
||||
const notifyOnError = (fn, errorToMessage) => async function (...args) {
|
||||
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
addNotification('error', errorToMessage(error));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const notifyOnError = (fn, errorToMessage) =>
|
||||
async function(...args) {
|
||||
if (typeof errorToMessage !== 'function') {
|
||||
errorToMessage = error => error.message;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
addNotification('error', errorToMessage(error));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const addBestTag = notifyOnError(() => addCommentTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG,
|
||||
}), () => 'Failed to tag comment as best');
|
||||
const addBestTag = notifyOnError(
|
||||
() =>
|
||||
addCommentTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG
|
||||
}),
|
||||
() => 'Failed to tag comment as best'
|
||||
);
|
||||
|
||||
const removeBestTag = notifyOnError(() => removeCommentTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG,
|
||||
}), () => 'Failed to remove best comment tag');
|
||||
const removeBestTag = notifyOnError(
|
||||
() =>
|
||||
removeCommentTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG
|
||||
}),
|
||||
() => 'Failed to remove best comment tag'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={commentClass}
|
||||
id={`c_${comment.id}`}
|
||||
style={{marginLeft: depth * 30}}>
|
||||
style={{marginLeft: depth * 30}}
|
||||
>
|
||||
<hr aria-hidden={true} />
|
||||
<div className={highlighted === comment.id ? 'highlighted-comment' : ''}>
|
||||
<AuthorName
|
||||
author={comment.user}/>
|
||||
{ isStaff(comment.tags)
|
||||
? <TagLabel>Staff</TagLabel>
|
||||
: null }
|
||||
<div
|
||||
className={highlighted === comment.id ? 'highlighted-comment' : ''}
|
||||
>
|
||||
<AuthorName author={comment.user} />
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
{ commentIsBest(comment)
|
||||
{commentIsBest(comment)
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
: null }
|
||||
: null}
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<Slot
|
||||
fill="commentInfoBar"
|
||||
@@ -186,47 +200,43 @@ class Comment extends React.Component {
|
||||
commentId={comment.id}
|
||||
inline
|
||||
/>
|
||||
{ (currentUser && (comment.user.id !== currentUser.id))
|
||||
{currentUser && comment.user.id !== currentUser.id
|
||||
? <span className={styles.topRightMenu}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
addNotification={addNotification} />
|
||||
addNotification={addNotification}
|
||||
/>
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
: null}
|
||||
|
||||
<Content body={comment.body} />
|
||||
<Slot fill="commentContent" />
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<Slot fill="commentReactions" inline />
|
||||
<ActionButton>
|
||||
{/* TODO implmement iPerformedThisAction for the like */}
|
||||
<LikeButton
|
||||
totalLikes={getTotalActionCount('LikeActionSummary', comment)}
|
||||
like={likeSummary[0]}
|
||||
id={comment.id}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
{
|
||||
!disableReply &&
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
comment={comment}
|
||||
commentId={comment.id}
|
||||
inline
|
||||
/>
|
||||
{!disableReply &&
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
</ActionButton>
|
||||
}
|
||||
banned={false}
|
||||
/>
|
||||
</ActionButton>}
|
||||
<ActionButton>
|
||||
<IfUserCanModifyBest user={currentUser}>
|
||||
<BestButton
|
||||
isBest={commentIsBest(comment)}
|
||||
addBest={addBestTag}
|
||||
removeBest={removeBestTag} />
|
||||
removeBest={removeBestTag}
|
||||
/>
|
||||
</IfUserCanModifyBest>
|
||||
</ActionButton>
|
||||
<Slot
|
||||
@@ -252,12 +262,12 @@ class Comment extends React.Component {
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
activeReplyBox === comment.id
|
||||
{activeReplyBox === comment.id
|
||||
? <ReplyBox
|
||||
commentPostedHandler={() => {
|
||||
setActiveReplyBox('');
|
||||
@@ -269,11 +279,10 @@ class Comment extends React.Component {
|
||||
addNotification={addNotification}
|
||||
authorId={currentUser.id}
|
||||
postItem={postItem}
|
||||
assetId={asset.id} />
|
||||
: null
|
||||
}
|
||||
{
|
||||
comment.replies &&
|
||||
assetId={asset.id}
|
||||
/>
|
||||
: null}
|
||||
{comment.replies &&
|
||||
comment.replies.map(reply => {
|
||||
return commentIsIgnored(reply)
|
||||
? <IgnoredCommentTombstone key={reply.id} />
|
||||
@@ -290,7 +299,6 @@ class Comment extends React.Component {
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
addCommentTag={addCommentTag}
|
||||
@@ -301,12 +309,11 @@ class Comment extends React.Component {
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply} />;
|
||||
})
|
||||
}
|
||||
{
|
||||
comment.replies &&
|
||||
<div className='coral-load-more-replies'>
|
||||
comment={reply}
|
||||
/>;
|
||||
})}
|
||||
{comment.replies &&
|
||||
<div className="coral-load-more-replies">
|
||||
<LoadMore
|
||||
assetId={asset.id}
|
||||
comments={comment.replies}
|
||||
@@ -314,9 +321,9 @@ class Comment extends React.Component {
|
||||
topLevel={false}
|
||||
replyCount={comment.replyCount}
|
||||
moreComments={comment.replyCount > comment.replies.length}
|
||||
loadMore={loadMore}/>
|
||||
</div>
|
||||
}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class Stream extends React.Component {
|
||||
postItem,
|
||||
addNotification,
|
||||
postFlag,
|
||||
postLike,
|
||||
postDontAgree,
|
||||
loadMore,
|
||||
deleteAction,
|
||||
@@ -127,7 +126,6 @@ class Stream extends React.Component {
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
loadMore={this.props.loadMore}
|
||||
@@ -165,7 +163,6 @@ class Stream extends React.Component {
|
||||
postItem={postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
addCommentTag={addCommentTag}
|
||||
|
||||
@@ -6,7 +6,7 @@ import uniqBy from 'lodash/uniqBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import isNil from 'lodash/isNil';
|
||||
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 {editName} from 'coral-framework/actions/user';
|
||||
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
|
||||
@@ -217,7 +217,6 @@ const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS(),
|
||||
commentCountCache: state.stream.commentCountCache,
|
||||
activeReplyBox: state.stream.activeReplyBox,
|
||||
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
assetUrl: state.stream.assetUrl,
|
||||
@@ -239,7 +238,6 @@ export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
postComment,
|
||||
postFlag,
|
||||
postLike,
|
||||
postDontAgree,
|
||||
addCommentTag,
|
||||
removeCommentTag,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
import POST_COMMENT from './postComment.graphql';
|
||||
import POST_FLAG from './postFlag.graphql';
|
||||
import POST_LIKE from './postLike.graphql';
|
||||
import POST_DONT_AGREE from './postDontAgree.graphql';
|
||||
import DELETE_ACTION from './deleteAction.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, {
|
||||
props: ({mutate}) => ({
|
||||
postFlag: (flag) => {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
mutation CreateLike ($like: CreateLikeInput!) {
|
||||
createLike(like:$like) {
|
||||
like {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -5,8 +5,6 @@ const Action = {
|
||||
return 'DontAgreeAction';
|
||||
case 'FLAG':
|
||||
return 'FlagAction';
|
||||
case 'LIKE':
|
||||
return 'LikeAction';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ const ActionSummary = {
|
||||
switch (action_type) {
|
||||
case 'FLAG':
|
||||
return 'FlagActionSummary';
|
||||
case 'LIKE':
|
||||
return 'LikeActionSummary';
|
||||
case 'DONTAGREE':
|
||||
return 'DontAgreeActionSummary';
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ const FlagAction = require('./flag_action');
|
||||
const DontAgreeAction = require('./dont_agree_action');
|
||||
const DontAgreeActionSummary = require('./dont_agree_action_summary');
|
||||
const GenericUserError = require('./generic_user_error');
|
||||
const LikeAction = require('./like_action');
|
||||
const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
const Settings = require('./settings');
|
||||
@@ -36,7 +35,6 @@ let resolvers = {
|
||||
DontAgreeAction,
|
||||
DontAgreeActionSummary,
|
||||
GenericUserError,
|
||||
LikeAction,
|
||||
RootMutation,
|
||||
RootQuery,
|
||||
Settings,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
const LikeAction = {
|
||||
|
||||
};
|
||||
|
||||
module.exports = LikeAction;
|
||||
@@ -5,9 +5,6 @@ const RootMutation = {
|
||||
createComment(_, {comment}, {mutators: {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}}) {
|
||||
return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}));
|
||||
},
|
||||
|
||||
@@ -103,9 +103,6 @@ enum COMMENT_STATUS {
|
||||
# The types of action there are as enum's.
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a LikeAction.
|
||||
LIKE
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
|
||||
@@ -295,41 +292,6 @@ type FlagAssetActionSummary implements AssetActionSummary {
|
||||
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.
|
||||
type FlagAction implements Action {
|
||||
|
||||
@@ -541,9 +503,6 @@ enum USER_STATUS {
|
||||
# Metrics for the assets.
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a LikeAction.
|
||||
LIKE
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
|
||||
@@ -629,15 +588,6 @@ enum ACTION_ITEM_TYPE {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
# The item's id for which we are to create a flag.
|
||||
@@ -786,9 +727,6 @@ type RootMutation {
|
||||
# Creates a comment on the asset.
|
||||
createComment(comment: CreateCommentInput!): CreateCommentResponse
|
||||
|
||||
# Creates a like on an entity.
|
||||
createLike(like: CreateLikeInput!): CreateLikeResponse
|
||||
|
||||
# Creates a flag on an entity.
|
||||
createFlag(flag: CreateFlagInput!): CreateFlagResponse
|
||||
|
||||
|
||||
@@ -136,6 +136,14 @@ const UserSchema = new mongoose.Schema({
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
},
|
||||
|
||||
toJSON: {
|
||||
transform: function (doc, ret) {
|
||||
delete ret.password;
|
||||
delete ret._id;
|
||||
delete ret.__v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,11 +3,16 @@ const path = require('path');
|
||||
const wrapResponse = require('../../graph/helpers/response');
|
||||
|
||||
module.exports = {
|
||||
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
|
||||
typeDefs: readFileSync(
|
||||
path.join(__dirname, 'server/typeDefs.graphql'),
|
||||
'utf8'
|
||||
),
|
||||
resolvers: {
|
||||
RootMutation: {
|
||||
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'})
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,9 +15,6 @@ describe('graph.loaders.Metrics', () => {
|
||||
describe('#Comments', () => {
|
||||
const query = `
|
||||
query CommentMetrics($from: Date!, $to: Date!) {
|
||||
liked: commentMetrics(from: $from, to: $to, sort: LIKE) {
|
||||
id
|
||||
}
|
||||
flagged: commentMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
id
|
||||
}
|
||||
@@ -33,26 +30,21 @@ describe('graph.loaders.Metrics', () => {
|
||||
]));
|
||||
|
||||
[
|
||||
{liked: 0, flagged: 0, actions: []},
|
||||
{liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}]},
|
||||
{liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
|
||||
{liked: 1, flagged: 1, actions: [
|
||||
{flagged: 0, actions: []},
|
||||
{flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'},
|
||||
{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}
|
||||
]},
|
||||
{liked: 3, 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'},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'}
|
||||
]}
|
||||
].forEach(({liked, flagged, actions}) => {
|
||||
].forEach(({flagged, actions}) => {
|
||||
|
||||
describe(`with actions=${actions.length}`, () => {
|
||||
|
||||
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']})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
@@ -60,8 +52,8 @@ describe('graph.loaders.Metrics', () => {
|
||||
to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
console.log(errors);
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.liked).to.have.length(liked);
|
||||
expect(data.flagged).to.have.length(flagged);
|
||||
});
|
||||
});
|
||||
@@ -78,7 +70,6 @@ describe('graph.loaders.Metrics', () => {
|
||||
fragment metrics on Asset {
|
||||
id
|
||||
action_summaries {
|
||||
type: __typename
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
@@ -88,9 +79,6 @@ describe('graph.loaders.Metrics', () => {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
...metrics
|
||||
}
|
||||
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
|
||||
...metrics
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -109,26 +97,21 @@ describe('graph.loaders.Metrics', () => {
|
||||
]));
|
||||
|
||||
[
|
||||
{liked: 0, flagged: 0, actions: []},
|
||||
{liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}]},
|
||||
{liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
|
||||
{liked: 1, flagged: 1, actions: [
|
||||
{flagged: 0, actions: []},
|
||||
{flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'},
|
||||
{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}
|
||||
]},
|
||||
{liked: 1, 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'},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'}
|
||||
]}
|
||||
].forEach(({liked, flagged, actions}) => {
|
||||
].forEach(({flagged, actions}) => {
|
||||
|
||||
describe(`with actions=${actions.length}`, () => {
|
||||
|
||||
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']})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
@@ -137,7 +120,6 @@ describe('graph.loaders.Metrics', () => {
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.assetsByLike).to.have.length(liked);
|
||||
expect(data.assetsByFlag).to.have.length(flagged);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user