mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 18:06:19 +08:00
Merge branch 'master' into all-comments-queue
This commit is contained in:
@@ -18,4 +18,6 @@ plugins.json
|
||||
plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:7.9
|
||||
FROM node:7.8
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@ FROM coralproject/talk:latest
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
# At this stage, we need to install the development dependancies again because
|
||||
# At this stage, we need to install the development dependancies again because
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
|
||||
NODE_ENV=production cli plugins reconcile && \
|
||||
NODE_ENV=production yarn build && \
|
||||
NODE_ENV=production yarn install --production && \
|
||||
NODE_ENV=production yarn install --production --force && \
|
||||
yarn cache clean
|
||||
+2
-2
@@ -174,8 +174,8 @@ and testing purposes.
|
||||
|
||||
There are some runtime requirements for running Talk from source:
|
||||
|
||||
- [Node](https://nodejs.org/) v7.9 or later
|
||||
- [Yarn](https://yarnpkg.com/) v0.22.0 or later
|
||||
- [Node](https://nodejs.org/) ~7.8
|
||||
- [Yarn](https://yarnpkg.com/) ^0.22.0
|
||||
|
||||
_Please be sure to check the versions of these requirements. Incorrect versions
|
||||
of these may lead to unexpected errors!_
|
||||
|
||||
@@ -116,7 +116,7 @@ class ModerationContainer extends Component {
|
||||
|
||||
let asset;
|
||||
|
||||
if (data.loading) {
|
||||
if (!('premodCount' in data)) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import FlagBox from './FlagBox';
|
||||
import CommentType from './CommentType';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import BanUserButton from 'coral-admin/src/components/BanUserButton';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
@@ -17,25 +18,27 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const Comment = ({actions = [], ...props}) => {
|
||||
const links = linkify.getMatches(props.comment.body);
|
||||
const Comment = ({actions = [], comment, ...props}) => {
|
||||
const links = linkify.getMatches(comment.body);
|
||||
const linkText = links ? links.map(link => link.raw) : [];
|
||||
const actionSummaries = props.comment.action_summaries;
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>
|
||||
{props.comment.user.name}
|
||||
{comment.user.name}
|
||||
</span>
|
||||
<span className={styles.created}>
|
||||
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
|
||||
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
|
||||
</span>
|
||||
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id, props.comment.status !== 'REJECTED')} />
|
||||
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
|
||||
<CommentType type={props.commentType} />
|
||||
</div>
|
||||
{props.comment.user.status === 'banned' ?
|
||||
{comment.user.status === 'banned' ?
|
||||
<span className={styles.banned}>
|
||||
<Icon name='error_outline'/>
|
||||
{lang.t('comment.banned_user')}
|
||||
@@ -43,16 +46,16 @@ const Comment = ({actions = [], ...props}) => {
|
||||
: null}
|
||||
</div>
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {props.comment.asset.title}
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset && (
|
||||
<Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate →</Link>
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate →</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<Highlighter
|
||||
searchWords={[...props.suspectWords, ...props.bannedWords, ...linkText]}
|
||||
textToHighlight={props.comment.body} />
|
||||
textToHighlight={comment.body} />
|
||||
</p>
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
@@ -60,16 +63,20 @@ const Comment = ({actions = [], ...props}) => {
|
||||
{actions.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action}
|
||||
user={props.comment.user}
|
||||
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
|
||||
user={comment.user}
|
||||
acceptComment={() => props.acceptComment({commentId: comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: comment.id})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
|
||||
{
|
||||
flagActions && flagActions.length
|
||||
? <FlagBox actions={flagActions} actionSummaries={flagActionSummaries} />
|
||||
: null
|
||||
}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -83,6 +90,7 @@ Comment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
user: PropTypes.shape({
|
||||
status: PropTypes.string
|
||||
|
||||
@@ -53,3 +53,17 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lessDetail {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.subDetail {
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
|
||||
span {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const shortReasons = {
|
||||
'This comment is offensive': lang.t('modqueue.offensive'),
|
||||
'This looks like an ad/marketing': lang.t('modqueue.spam/ads'),
|
||||
'This user is impersonating': lang.t('modqueue.impersonating'),
|
||||
'I don\'t like this username': lang.t('modqueue.dont-like-username'),
|
||||
'Other': lang.t('modqueue.other')
|
||||
};
|
||||
|
||||
class FlagBox extends Component {
|
||||
constructor () {
|
||||
@@ -16,27 +27,50 @@ class FlagBox extends Component {
|
||||
}));
|
||||
}
|
||||
|
||||
reasonMap = (reason) => {
|
||||
const shortReason = shortReasons[reason];
|
||||
|
||||
// if the short reason isn't found, just return the long one.
|
||||
return shortReason ? shortReason : reason;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const {actionSummaries, actions} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>Flags ({props.actionSummaries.length}):</h3>
|
||||
<Icon name='flag'/><h3>Flags ({actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
{actionSummaries.map((action, i) =>
|
||||
<li key={i} className={styles.lessDetail}>{this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
{/* <a onClick={this.toggleDetail} className={styles.moreDetail}>More detail</a>*/}
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? lang.t('modqueue.less-detail') : lang.t('modqueue.more-detail')}</a>
|
||||
</div>
|
||||
{this.state.showDetail && (<div className={styles.detail}>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>)}
|
||||
{showDetail && (
|
||||
<div className={styles.detail}>
|
||||
<ul>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
|
||||
const actionList = actions.filter(a => a.reason === summary.reason);
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{
|
||||
actionList.map((action, j) => <li key={`${i}_${j}`} className={styles.subDetail}><span>{action.user.username}</span> {action.message}</li>)
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -44,7 +78,14 @@ class FlagBox extends Component {
|
||||
}
|
||||
|
||||
FlagBox.propTypes = {
|
||||
actionSummaries: PropTypes.array.isRequired
|
||||
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
|
||||
reason: PropTypes.string,
|
||||
count: PropTypes.number
|
||||
})).isRequired,
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
user: PropTypes.shape({username: PropTypes.string})
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default FlagBox;
|
||||
|
||||
@@ -12,4 +12,19 @@ fragment commentView on Comment {
|
||||
id
|
||||
title
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
actions {
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) =>
|
||||
statuses,
|
||||
asset_id
|
||||
},
|
||||
updateQuery: (oldData, {fetchMoreResult:{data:{comments}}}) => {
|
||||
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
|
||||
return {
|
||||
...oldData,
|
||||
[tab]: [
|
||||
|
||||
@@ -21,12 +21,6 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
rejected: comments(query: {
|
||||
statuses: [REJECTED],
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
import fragmentMatcher from './fragmentMatcher';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
fragmentMatcher,
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {IntrospectionFragmentMatcher} from 'apollo-client';
|
||||
|
||||
// TODO this is a short-term fix
|
||||
// we need to set up something to query the server for the schema before ApolloClient initialization
|
||||
// https://github.com/apollographql/apollo-client/issues/1555#issuecomment-295834774
|
||||
const fm = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData: {
|
||||
__schema: {
|
||||
types: [
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'Action',
|
||||
possibleTypes: [
|
||||
{name: 'FlagAction'},
|
||||
{name: 'LikeAction'},
|
||||
{name: 'DontAgreeAction'}
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'ActionSummary',
|
||||
possibleTypes: [
|
||||
{name: 'FlagActionSummary'},
|
||||
{name: 'LikeActionSummary'},
|
||||
{name: 'DontAgreeActionSummary'}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default fm;
|
||||
@@ -51,7 +51,14 @@
|
||||
"singleview": "Toggle single comment edit view",
|
||||
"thismenu": "Open this menu",
|
||||
"emptyqueue": "No more comments to moderate! You're all caught up. Go have some ☕️",
|
||||
"showshortcuts": "Show Shortcuts"
|
||||
"showshortcuts": "Show Shortcuts",
|
||||
"more-detail": "More detail",
|
||||
"less-detail": "Less detail",
|
||||
"dont-like-username": "Don't like username",
|
||||
"impersonating": "Impersonating",
|
||||
"offensive": "Offensive",
|
||||
"spam/ads": "Spam/Ads",
|
||||
"other": "Other"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
@@ -221,7 +228,14 @@
|
||||
"shortcuts": "Atajos de teclado",
|
||||
"close": "Cerrar",
|
||||
"emptyqueue": "No se encontro ningún usuario. Están escondidos.",
|
||||
"showshortcuts": "Mostrar atajos"
|
||||
"showshortcuts": "Mostrar atajos",
|
||||
"more-detail": "Mas detalle",
|
||||
"less-detail": "Menos detalle",
|
||||
"dont-like-username": "No me gusta ese nombre de usuario",
|
||||
"impersonating": "Suplantación",
|
||||
"offensive": "Ofensivo",
|
||||
"spam/ads": "Spam/Propaganda",
|
||||
"other": "Otros"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, browserHistory} from 'react-router';
|
||||
|
||||
import Embed from './Embed';
|
||||
import SignInContainer from 'coral-sign-in/containers/SignInContainer';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/embed/stream/login" component={SignInContainer}/>
|
||||
<Route exact path="*" component={Embed}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
|
||||
|
||||
export default AppRouter;
|
||||
@@ -19,14 +19,13 @@ 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 LoadMore from 'coral-embed-stream/src/LoadMore';
|
||||
import {Slot} from 'coral-framework';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import {TopRightMenu} from './TopRightMenu';
|
||||
import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
const getActionSummary = (type, comment) => comment.action_summaries
|
||||
.filter((a) => a.__typename === type)[0];
|
||||
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
|
||||
|
||||
// hold actions links (e.g. Like, Reply) along the comment footer
|
||||
@@ -124,9 +123,16 @@ class Comment extends React.Component {
|
||||
commentIsIgnored,
|
||||
} = this.props;
|
||||
|
||||
const like = getActionSummary('LikeActionSummary', comment);
|
||||
const flag = getActionSummary('FlagActionSummary', comment);
|
||||
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
|
||||
const likeSummary = getActionSummary('LikeActionSummary', comment);
|
||||
const flagSummary = getActionSummary('FlagActionSummary', comment);
|
||||
const dontAgreeSummary = getActionSummary('DontAgreeActionSummary', comment);
|
||||
let myFlag = null;
|
||||
if (iPerformedThisAction('FlagActionSummary', comment)) {
|
||||
myFlag = flagSummary.find(s => s.current_user);
|
||||
} else if (iPerformedThisAction('DontAgreeActionSummary', comment)) {
|
||||
myFlag = dontAgreeSummary.find(s => s.current_user);
|
||||
}
|
||||
|
||||
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
|
||||
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
|
||||
|
||||
@@ -168,7 +174,7 @@ class Comment extends React.Component {
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
: null }
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<Slot fill="commentInfoBar" commentId={comment.id} />
|
||||
<Slot fill="commentInfoBar" comment={comment} commentId={comment.id} inline/>
|
||||
|
||||
{ (currentUser && (comment.user.id !== currentUser.id))
|
||||
? <span className={styles.topRightMenu}>
|
||||
@@ -183,8 +189,10 @@ class Comment extends React.Component {
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<ActionButton>
|
||||
{/* TODO implmement iPerformedThisAction for the like */}
|
||||
<LikeButton
|
||||
like={like}
|
||||
totalLikes={getTotalActionCount('LikeActionSummary', comment)}
|
||||
like={likeSummary[0]}
|
||||
id={comment.id}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
@@ -209,7 +217,7 @@ class Comment extends React.Component {
|
||||
removeBest={removeBestTag} />
|
||||
</IfUserCanModifyBest>
|
||||
</ActionButton>
|
||||
<Slot fill="commentDetail" commentId={comment.id} />
|
||||
<Slot fill="commentDetail" comment={comment} commentId={comment.id} inline/>
|
||||
</div>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<ActionButton>
|
||||
@@ -217,7 +225,8 @@ class Comment extends React.Component {
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flag={flag && flag.current_user ? flag : dontagree}
|
||||
flaggedByCurrentUser={!!myFlag}
|
||||
flag={myFlag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
|
||||
@@ -8,7 +8,7 @@ const lang = new I18n(translations);
|
||||
|
||||
import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui';
|
||||
|
||||
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
|
||||
const {logout, showSignInDialog, requestConfirmEmail, openSignInPopUp, checkLogin} = authActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
|
||||
@@ -26,7 +26,6 @@ import {ModerationLink} from 'coral-plugin-moderation';
|
||||
import Count from 'coral-plugin-comment-count/CommentCount';
|
||||
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
||||
import UserBox from 'coral-sign-in/components/UserBox';
|
||||
import SignInContainer from 'coral-sign-in/containers/SignInContainer';
|
||||
import SuspendedAccount from 'coral-framework/components/SuspendedAccount';
|
||||
import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUsernameContainer';
|
||||
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
|
||||
@@ -78,6 +77,7 @@ class Embed extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
pym.sendMessage('childReady');
|
||||
this.props.checkLogin();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@@ -119,8 +119,7 @@ class Embed extends React.Component {
|
||||
|
||||
setActiveReplyBox = (reactKey) => {
|
||||
if (!this.props.auth.user) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
this.props.showSignInDialog();
|
||||
} else {
|
||||
this.setState({activeReplyBox: reactKey});
|
||||
}
|
||||
@@ -130,7 +129,7 @@ class Embed extends React.Component {
|
||||
const {activeTab} = this.state;
|
||||
const {closedAt, countCache = {}} = this.props.asset;
|
||||
const {asset, refetch, comment} = this.props.data;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog} = this.props.auth;
|
||||
|
||||
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
|
||||
const highlightedComment = comment && comment.parent ? comment.parent : comment;
|
||||
@@ -218,11 +217,10 @@ class Embed extends React.Component {
|
||||
</div>
|
||||
: <p>{asset.settings.closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer
|
||||
requireEmailConfirmation={asset.settings.requireEmailConfirmation}
|
||||
refetch={refetch}
|
||||
offset={signInOffset}/>}
|
||||
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
|
||||
|
||||
{!loggedIn && <Button id='coralSignInButton' onClick={this.props.showSignInDialog} full>Sign in to comment</Button>}
|
||||
|
||||
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} user={user} />}
|
||||
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
|
||||
|
||||
{/* the highlightedComment is isolated after the user followed a permalink */}
|
||||
@@ -290,7 +288,6 @@ class Embed extends React.Component {
|
||||
<ProfileContainer
|
||||
loggedIn={loggedIn}
|
||||
userData={this.props.userData}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 2}>
|
||||
@@ -320,10 +317,12 @@ const mapDispatchToProps = dispatch => ({
|
||||
addNotification: (type, text) => addNotification(type, text),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
editName: (username) => dispatch(editName(username)),
|
||||
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
|
||||
showSignInDialog: () => dispatch(showSignInDialog()),
|
||||
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
|
||||
viewAllComments: () => dispatch(viewAllComments()),
|
||||
logout: () => dispatch(logout()),
|
||||
openSignInPopUp: cb => dispatch(openSignInPopUp(cb)),
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
dispatch: d => dispatch(d),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from 'coral-framework/services/client';
|
||||
import store from 'coral-framework/services/store';
|
||||
import localStore from 'coral-framework/services/store';
|
||||
|
||||
import Embed from './Embed';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
const store = (window.opener && window.opener.coralStore) ? window.opener.coralStore : localStore;
|
||||
|
||||
render(
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<Embed />
|
||||
<AppRouter />
|
||||
</ApolloProvider>
|
||||
, document.querySelector('#coralStream')
|
||||
);
|
||||
|
||||
@@ -396,6 +396,7 @@ button.comment__action-button[disabled],
|
||||
margin-left: 20px;
|
||||
margin-top: 5px;
|
||||
width: 75%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Close comments */
|
||||
@@ -475,3 +476,19 @@ button.comment__action-button[disabled],
|
||||
.coral-load-more-replies button.coral-load-more, .coral-new-comments button.coral-load-more{
|
||||
width: initial;
|
||||
}
|
||||
|
||||
@media (min-device-width : 300px) and (max-device-width : 420px) {
|
||||
.commentActionsLeft.comment__action-container .coral-plugin-likes-button-text,
|
||||
.commentActionsLeft.comment__action-container > div span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button {
|
||||
visibility: collapse;
|
||||
margin-left: -30px;
|
||||
}
|
||||
|
||||
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button .coral-plugin-replies-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@ const ME_QUERY = gql`
|
||||
query Me {
|
||||
me {
|
||||
status
|
||||
comments {
|
||||
id
|
||||
body
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -22,8 +32,23 @@ function fetchMe() {
|
||||
}
|
||||
|
||||
// Dialog Actions
|
||||
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
|
||||
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
export const showSignInDialog = () => dispatch => {
|
||||
const signInPopUp = window.open(
|
||||
'/embed/stream/login',
|
||||
'Login',
|
||||
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
|
||||
);
|
||||
|
||||
signInPopUp.onbeforeunload = () => {
|
||||
dispatch(checkLogin());
|
||||
fetchMe();
|
||||
};
|
||||
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
|
||||
};
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
window.close();
|
||||
};
|
||||
|
||||
export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST});
|
||||
export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG});
|
||||
@@ -47,29 +72,39 @@ export const createUsername = (userId, formData) => dispatch => {
|
||||
});
|
||||
};
|
||||
|
||||
export const changeView = view => dispatch =>
|
||||
export const changeView = view => dispatch => {
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
view
|
||||
});
|
||||
|
||||
switch(view) {
|
||||
case 'SIGNUP':
|
||||
window.resizeTo(500, 800);
|
||||
break;
|
||||
case 'FORGOT':
|
||||
window.resizeTo(500, 400);
|
||||
break;
|
||||
default:
|
||||
window.resizeTo(500, 550);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanState = () => ({type: actions.CLEAN_STATE});
|
||||
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
|
||||
const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
|
||||
|
||||
// TODO: revisit login redux flow.
|
||||
// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
|
||||
//
|
||||
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
|
||||
export const fetchSignIn = (formData) => (dispatch) => {
|
||||
dispatch(signInRequest());
|
||||
return coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(signInSuccess(user, isAdmin));
|
||||
dispatch(hideSignInDialog());
|
||||
fetchMe();
|
||||
})
|
||||
.then(() => dispatch(hideSignInDialog()))
|
||||
.catch(error => {
|
||||
if (error.metadata) {
|
||||
|
||||
@@ -121,7 +156,7 @@ export const facebookCallback = (err, data) => dispatch => {
|
||||
dispatch(signInFacebookSuccess(user));
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(showCreateUsernameDialog());
|
||||
fetchMe();
|
||||
dispatch(hideSignInDialog());
|
||||
} catch (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import pym from '../services/PymConnection';
|
||||
import * as actions from '../constants/notification';
|
||||
|
||||
export const addNotification = (notifType, text) => {
|
||||
pym.sendMessage('coral-alert', `${notifType}|${text}`);
|
||||
return {type: actions.ADD_NOTIFICATION, notifType, text};
|
||||
};
|
||||
|
||||
export const clearNotification = () => {
|
||||
pym.sendMessage('coral-clear-notification');
|
||||
return {type: actions.CLEAR_NOTIFICATION};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, {Component} from 'react';
|
||||
import {getSlotElements} from 'coral-framework/helpers/plugins';
|
||||
import styles from './Slot.css';
|
||||
|
||||
class Slot extends Component {
|
||||
render() {
|
||||
const {fill, ...rest} = this.props;
|
||||
const {fill, inline = false, ...rest} = this.props;
|
||||
return (
|
||||
<span>
|
||||
<div className={inline ? styles.inline : ''}>
|
||||
{getSlotElements(fill, rest)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
|
||||
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
|
||||
@@ -20,12 +20,11 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
fragments: commentView
|
||||
}),
|
||||
props: ({ownProps, mutate}) => ({
|
||||
postItem: ({asset_id, body, parent_id}) =>
|
||||
mutate({
|
||||
postItem: comment => {
|
||||
const {asset_id, body, parent_id, tags = []} = comment;
|
||||
return mutate({
|
||||
variables: {
|
||||
asset_id,
|
||||
body,
|
||||
parent_id
|
||||
comment
|
||||
},
|
||||
optimisticResponse: {
|
||||
createComment: {
|
||||
@@ -39,14 +38,14 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
parent_id,
|
||||
asset_id,
|
||||
action_summaries: [],
|
||||
tags: [],
|
||||
tags,
|
||||
status: null,
|
||||
id: 'pending'
|
||||
}
|
||||
}
|
||||
},
|
||||
updateQueries: {
|
||||
AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => {
|
||||
AssetQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
|
||||
|
||||
if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
|
||||
return oldData;
|
||||
@@ -62,8 +61,8 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
...oldData.asset,
|
||||
comments: oldData.asset.comments.map((oldComment) => {
|
||||
return oldComment.id === parent_id
|
||||
? {...oldComment, replies: [...oldComment.replies, comment]}
|
||||
: oldComment;
|
||||
? {...oldComment, replies: [...oldComment.replies, comment]}
|
||||
: oldComment;
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -83,7 +82,8 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
return updatedAsset;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
|
||||
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
|
||||
mutation CreateComment ($comment: CreateCommentInput!) {
|
||||
createComment(comment: $comment) {
|
||||
comment {
|
||||
...commentView
|
||||
replyCount
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import store from './services/store';
|
||||
import pym from './services/PymConnection';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import actions from './actions';
|
||||
import Slot from './components/Slot';
|
||||
|
||||
// TODO (bc): Deprecate old actions. Spreading actions is now needed.
|
||||
|
||||
export default {
|
||||
pym,
|
||||
Slot,
|
||||
I18n,
|
||||
store,
|
||||
actions,
|
||||
...actions
|
||||
};
|
||||
|
||||
@@ -28,8 +28,7 @@ export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SIGNIN_DIALOG :
|
||||
return state
|
||||
.set('showSignInDialog', true)
|
||||
.set('signInOffset', action.offset);
|
||||
.set('showSignInDialog', true);
|
||||
case actions.HIDE_SIGNIN_DIALOG :
|
||||
return state.merge(Map({
|
||||
isLoading: false,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
import asset from './asset';
|
||||
import {reducer as commentBox} from '../../coral-plugin-commentbox';
|
||||
import {pluginReducers} from '../helpers/plugins';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
user,
|
||||
asset,
|
||||
commentBox,
|
||||
...pluginReducers
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ if (window.devToolsExtension) {
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
export default createStore(
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
...mainReducer,
|
||||
apollo: client.reducer()
|
||||
@@ -32,3 +32,6 @@ export default createStore(
|
||||
{},
|
||||
compose(...middlewares)
|
||||
);
|
||||
|
||||
export default store;
|
||||
window.coralStore = store;
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
/**
|
||||
* getActionSummary
|
||||
* retrieves the action summary based on the type and the comment
|
||||
*/
|
||||
export const getTotalActionCount = (type, comment) => {
|
||||
return comment.action_summaries
|
||||
.filter(s => s.__typename === type)
|
||||
.reduce((total, summary) => {
|
||||
return total + summary.count;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const getActionSummary = (type, comment) =>
|
||||
comment.action_summaries.filter(a => a.__typename === type)[0];
|
||||
export const iPerformedThisAction = (type, comment) => {
|
||||
|
||||
// if there is a current_user on any of the ActionSummary(s), the user performed this action
|
||||
return comment.action_summaries
|
||||
.filter(a => a.__typename === type)
|
||||
.some(a => a.current_user);
|
||||
};
|
||||
|
||||
export const getMyActionSummary = (type, comment) => {
|
||||
return comment.action_summaries
|
||||
.filter(a => a.__typename === type)
|
||||
.find(a => a.current_user);
|
||||
};
|
||||
|
||||
/**
|
||||
* getActionSummary
|
||||
* retrieves the action summaries based on the type and the comment
|
||||
* array could be length > 1, as in the case of FlagActionSummary
|
||||
*/
|
||||
|
||||
export const getActionSummary = (type, comment) => {
|
||||
return comment.action_summaries.filter(a => a.__typename === type);
|
||||
};
|
||||
|
||||
@@ -2,56 +2,57 @@ import React, {Component, PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import {Button} from 'coral-ui';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
const name = 'coral-plugin-commentbox';
|
||||
|
||||
class CommentBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
cancelButtonClicked: PropTypes.func,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
authorId: PropTypes.string.isRequired,
|
||||
isReply: PropTypes.bool.isRequired,
|
||||
canPost: PropTypes.bool,
|
||||
currentUser: PropTypes.object
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
state = {
|
||||
body: '',
|
||||
username: ''
|
||||
this.state = {
|
||||
username: '',
|
||||
body: '',
|
||||
hooks: {
|
||||
preSubmit: [],
|
||||
postSubmit: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
postComment = () => {
|
||||
const {
|
||||
commentPostedHandler,
|
||||
postItem,
|
||||
assetId,
|
||||
updateCountCache,
|
||||
isReply,
|
||||
countCache,
|
||||
assetId,
|
||||
parentId,
|
||||
postItem,
|
||||
countCache,
|
||||
addNotification,
|
||||
authorId
|
||||
updateCountCache,
|
||||
commentPostedHandler
|
||||
} = this.props;
|
||||
|
||||
let comment = {
|
||||
body: this.state.body,
|
||||
asset_id: assetId,
|
||||
author_id: authorId,
|
||||
parent_id: parentId
|
||||
parent_id: parentId,
|
||||
body: this.state.body,
|
||||
...this.props.commentBox
|
||||
};
|
||||
|
||||
if (this.props.charCount && this.state.body.length > this.props.charCount) {
|
||||
return;
|
||||
}
|
||||
!isReply && updateCountCache(assetId, countCache + 1);
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.state.hooks.preSubmit.forEach(hook => hook());
|
||||
|
||||
postItem(comment, 'comments')
|
||||
.then(({data}) => {
|
||||
const postedComment = data.createComment.comment;
|
||||
|
||||
// Execute postSubmit Hooks
|
||||
this.state.hooks.postSubmit.forEach(hook => hook(data));
|
||||
|
||||
if (postedComment.status === 'REJECTED') {
|
||||
addNotification('error', lang.t('comment-post-banned-word'));
|
||||
!isReply && updateCountCache(assetId, countCache);
|
||||
@@ -64,14 +65,67 @@ class CommentBox extends Component {
|
||||
commentPostedHandler();
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
this.setState({body: ''});
|
||||
}
|
||||
|
||||
registerHook = (hookType = '', hook = () => {}) => {
|
||||
if (typeof hook !== 'function') {
|
||||
return console.warn(`Hooks must be functions. Please check your ${hookType} hooks`);
|
||||
} else if (typeof hookType === 'string') {
|
||||
this.setState(state => ({
|
||||
hooks: {
|
||||
...state.hooks,
|
||||
[hookType]: [
|
||||
...state.hooks[hookType],
|
||||
hook
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
hookType,
|
||||
hook
|
||||
};
|
||||
|
||||
} else {
|
||||
return console.warn('hookTypes must be a string. Please check your hooks');
|
||||
}
|
||||
}
|
||||
|
||||
unregisterHook = hookData => {
|
||||
const {hookType, hook} = hookData;
|
||||
|
||||
this.setState(state => {
|
||||
let newHooks = state.hooks[newHooks];
|
||||
const idx = state.hooks[hookType].indexOf(hook);
|
||||
|
||||
if (idx !== -1) {
|
||||
newHooks = [
|
||||
...state.hooks[hookType].slice(0, idx),
|
||||
...state.hooks[hookType].slice(idx + 1)
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
...state.hooks,
|
||||
[hookType]: newHooks
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
handleChange = e => this.setState({body: e.target.value});
|
||||
|
||||
render () {
|
||||
const {styles, isReply, authorId, charCount} = this.props;
|
||||
let {cancelButtonClicked} = this.props;
|
||||
|
||||
const length = this.state.body.length;
|
||||
const enablePostComment = !length || (charCount && length > charCount);
|
||||
|
||||
if (isReply && typeof cancelButtonClicked !== 'function') {
|
||||
console.warn('the CommentBox component should have a cancelButtonClicked callback defined if it lives in a Reply');
|
||||
@@ -93,33 +147,35 @@ class CommentBox extends Component {
|
||||
value={this.state.body}
|
||||
placeholder={lang.t('comment')}
|
||||
id={isReply ? 'replyText' : 'commentText'}
|
||||
onChange={(e) => this.setState({body: e.target.value})}
|
||||
onChange={this.handleChange}
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={`${name}-char-count ${length > charCount ? `${name}-char-max` : ''}`}>
|
||||
{
|
||||
charCount &&
|
||||
`${charCount - length} ${lang.t('characters-remaining')}`
|
||||
}
|
||||
{charCount && `${charCount - length} ${lang.t('characters-remaining')}`}
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
<Slot
|
||||
fill="commentBoxDetail"
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
inline
|
||||
/>
|
||||
{
|
||||
isReply && (
|
||||
<Button
|
||||
cStyle='darkGrey'
|
||||
className={`${name}-cancel-button`}
|
||||
onClick={() => {
|
||||
cancelButtonClicked('');
|
||||
}}>
|
||||
onClick={() => cancelButtonClicked('')}>
|
||||
{lang.t('cancel')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{ authorId && (
|
||||
<Button
|
||||
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
|
||||
cStyle={enablePostComment ? 'lightGrey' : 'darkGrey'}
|
||||
className={`${name}-button`}
|
||||
onClick={this.postComment}>
|
||||
onClick={this.postComment}
|
||||
disabled={enablePostComment ? 'disabled' : ''}>
|
||||
{lang.t('post')}
|
||||
</Button>
|
||||
)
|
||||
@@ -129,6 +185,20 @@ class CommentBox extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentBox;
|
||||
CommentBox.propTypes = {
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
cancelButtonClicked: PropTypes.func,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
authorId: PropTypes.string.isRequired,
|
||||
isReply: PropTypes.bool.isRequired,
|
||||
canPost: PropTypes.bool,
|
||||
currentUser: PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
export default connect(mapStateToProps, null)(CommentBox);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const addTag = tag => ({
|
||||
type: 'ADD_TAG',
|
||||
tag
|
||||
});
|
||||
|
||||
export const removeTag = idx => ({
|
||||
type: 'REMOVE_TAG',
|
||||
idx
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const ADD_TAG = 'ADD_TAG';
|
||||
export const REMOVE_TAG = 'REMOVE_TAG';
|
||||
@@ -0,0 +1,5 @@
|
||||
import reducer from './reducer';
|
||||
|
||||
export default {
|
||||
reducer
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import {ADD_TAG, REMOVE_TAG} from './constants';
|
||||
|
||||
const initialState = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default function commentBox (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case ADD_TAG :
|
||||
return {
|
||||
...state,
|
||||
tags: [...state.tags, action.tag]
|
||||
};
|
||||
case REMOVE_TAG :
|
||||
return {
|
||||
...state,
|
||||
tags: [
|
||||
...state.tags.slice(0, action.idx),
|
||||
...state.tags.slice(action.idx + 1)
|
||||
]
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.slot {
|
||||
display: inline-block;
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,14 @@ class FlagButton extends Component {
|
||||
|
||||
// When the "report" button is clicked expand the menu
|
||||
onReportClick = () => {
|
||||
const {currentUser, flag, deleteAction} = this.props;
|
||||
const {currentUser, deleteAction, flaggedByCurrentUser, flag} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current_user && !localDelete) || localPost;
|
||||
const localFlagged = (flaggedByCurrentUser && !localDelete) || localPost;
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
this.props.showSignInDialog();
|
||||
return;
|
||||
}
|
||||
if (flagged) {
|
||||
if (localFlagged) {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || flag.current_user.id);
|
||||
} else if (this.state.showMenu){
|
||||
@@ -130,9 +129,9 @@ class FlagButton extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {flag, getPopupMenu} = this.props;
|
||||
const {getPopupMenu, flaggedByCurrentUser} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current_user && !localDelete) || localPost;
|
||||
const flagged = (flaggedByCurrentUser && !localDelete) || localPost;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
|
||||
@@ -27,16 +27,15 @@ class LikeButton extends Component {
|
||||
|
||||
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;
|
||||
let count = like ? like.count : 0;
|
||||
if (localPost) {count += 1;}
|
||||
if (localDelete) {count -= 1;}
|
||||
|
||||
const onLikeClick = () => {
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
|
||||
showSignInDialog(offset);
|
||||
showSignInDialog();
|
||||
return;
|
||||
}
|
||||
if (currentUser.banned) {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './NotLoggedIn.css';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default ({showSignInDialog, requireEmailConfirmation}) => (
|
||||
export default ({showSignInDialog}) => (
|
||||
<div className={styles.message}>
|
||||
<SignInContainer noButton={true} requireEmailConfirmation={requireEmailConfirmation}/>
|
||||
<div>
|
||||
<a onClick={() => {
|
||||
showSignInDialog();
|
||||
}}>{lang.t('signIn')}</a> {lang.t('toAccess')}
|
||||
<a onClick={showSignInDialog}>{lang.t('signIn')}</a> {lang.t('toAccess')}
|
||||
</div>
|
||||
<div>
|
||||
{lang.t('fromSettingsPage')}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {connect} from 'react-redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import React, {Component} from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {myCommentHistory, myIgnoredUsers} from 'coral-framework/graphql/queries';
|
||||
import {stopIgnoringUser} from 'coral-framework/graphql/mutations';
|
||||
@@ -12,6 +13,8 @@ import IgnoredUsers from '../components/IgnoredUsers';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import CommentHistory from 'coral-plugin-history/CommentHistory';
|
||||
|
||||
import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth';
|
||||
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -32,17 +35,17 @@ class ProfileContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {loggedIn, asset, showSignInDialog, data, myIgnoredUsersData, stopIgnoringUser} = this.props;
|
||||
const {loggedIn, asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props;
|
||||
const {me} = this.props.data;
|
||||
|
||||
if (!loggedIn || !me) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} requireEmailConfirmation={asset.settings.requireEmailConfirmation}/>;
|
||||
}
|
||||
|
||||
if (data.loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
if (!loggedIn || !me) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
const localProfile = this.props.user.profiles.find(p => p.provider === 'local');
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
|
||||
@@ -81,7 +84,6 @@ class ProfileContainer extends Component {
|
||||
:
|
||||
<p>{lang.t('userNoComment')}</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -93,10 +95,8 @@ const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({
|
||||
|
||||
// saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData))
|
||||
});
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({showSignInDialog, checkLogin}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
|
||||
@@ -10,16 +10,12 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmitUsername, handleChange, ...props}) => {
|
||||
const CreateUsernameDialog = ({open, handleClose, formData, handleSubmitUsername, handleChange, ...props}) => {
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.dialogusername}
|
||||
id="createUsernameDialog"
|
||||
open={open}
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: offset !== 0 && offset
|
||||
}}>
|
||||
open={open}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
@@ -42,6 +38,7 @@ const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmit
|
||||
<div className={styles.saveusername}>
|
||||
<TextField
|
||||
id="username"
|
||||
style={{fontSize: 16}}
|
||||
type="string"
|
||||
label={lang.t('createdisplay.username')}
|
||||
value={formData.username}
|
||||
|
||||
@@ -31,6 +31,7 @@ class ForgotContent extends React.Component {
|
||||
<input
|
||||
ref={input => this.emailInput = input}
|
||||
type="text"
|
||||
style={{fontSize: 16}}
|
||||
id="email"
|
||||
name="email" />
|
||||
</div>
|
||||
|
||||
@@ -6,15 +6,11 @@ import SignInContent from './SignInContent';
|
||||
import SignUpContent from './SignUpContent';
|
||||
import ForgotContent from './ForgotContent';
|
||||
|
||||
const SignDialog = ({open, view, handleClose, offset, ...props}) => (
|
||||
const SignDialog = ({open, view, handleClose, ...props}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="signInDialog"
|
||||
open={open}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: offset !== 0 && offset
|
||||
}}>
|
||||
open={open}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
{view === 'SIGNIN' && <SignInContent {...props} />}
|
||||
{view === 'SIGNUP' && <SignUpContent {...props} />}
|
||||
|
||||
@@ -20,8 +20,8 @@ const SignInContent = ({
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<div className="coral-sign-in">
|
||||
<div className={`${styles.header} header`}>
|
||||
<h1>
|
||||
{auth.emailVerificationFailure ? lang.t('signIn.emailVerifyCTA') : lang.t('signIn.signIn')}
|
||||
</h1>
|
||||
@@ -42,7 +42,7 @@ const SignInContent = ({
|
||||
{emailVerificationSuccess && <Success />}
|
||||
</form>
|
||||
: <div>
|
||||
<div className={styles.socialConnections}>
|
||||
<div className={`${styles.socialConnections} social-connections`}>
|
||||
<Button cStyle="facebook" onClick={fetchSignInFacebook} full>
|
||||
{lang.t('signIn.facebookSignIn')}
|
||||
</Button>
|
||||
@@ -58,6 +58,7 @@ const SignInContent = ({
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
style={{fontSize: 16}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
@@ -65,6 +66,7 @@ const SignInContent = ({
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
value={formData.password}
|
||||
style={{fontSize: 16}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
@@ -80,7 +82,7 @@ const SignInContent = ({
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.footer}>
|
||||
<div className={`${styles.footer} footer`}>
|
||||
<span><a onClick={() => changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
|
||||
<span>
|
||||
{lang.t('signIn.needAnAccount')}
|
||||
|
||||
@@ -83,6 +83,7 @@ class SignUpContent extends React.Component {
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
style={{fontSize: 16}}
|
||||
showErrors={showErrors}
|
||||
errorMsg={errors.email}
|
||||
onChange={handleChange}
|
||||
@@ -93,6 +94,7 @@ class SignUpContent extends React.Component {
|
||||
label={lang.t('signIn.username')}
|
||||
value={formData.username}
|
||||
showErrors={showErrors}
|
||||
style={{fontSize: 16}}
|
||||
errorMsg={errors.username}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
@@ -102,6 +104,7 @@ class SignUpContent extends React.Component {
|
||||
label={lang.t('signIn.password')}
|
||||
value={formData.password}
|
||||
showErrors={showErrors}
|
||||
style={{fontSize: 16}}
|
||||
errorMsg={errors.password}
|
||||
onChange={handleChange}
|
||||
minLength="8"
|
||||
@@ -112,6 +115,7 @@ class SignUpContent extends React.Component {
|
||||
type="password"
|
||||
label={lang.t('signIn.confirmPassword')}
|
||||
value={formData.confirmPassword}
|
||||
style={{fontSize: 16}}
|
||||
showErrors={showErrors}
|
||||
errorMsg={errors.confirmPassword}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -100,12 +100,11 @@ class ChangeUsernameContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {loggedIn, auth, offset} = this.props;
|
||||
const {loggedIn, auth} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<CreateUsernameDialog
|
||||
open={auth.showCreateUsernameDialog && auth.user.canEditName}
|
||||
offset={offset}
|
||||
handleClose={this.handleClose}
|
||||
loggedIn={loggedIn}
|
||||
handleSubmitUsername={this.handleSubmitUsername}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import SignDialog from '../components/SignDialog';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
changeView,
|
||||
fetchSignUp,
|
||||
fetchSignIn,
|
||||
showSignInDialog,
|
||||
hideSignInDialog,
|
||||
fetchSignInFacebook,
|
||||
fetchSignUpFacebook,
|
||||
@@ -147,23 +145,18 @@ class SignInContainer extends Component {
|
||||
|
||||
handleSignIn(e) {
|
||||
e.preventDefault();
|
||||
this.props.fetchSignIn(this.state.formData)
|
||||
.then(this.props.refetch);
|
||||
this.props.fetchSignIn(this.state.formData);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {auth, showSignInDialog, noButton, offset, requireEmailConfirmation} = this.props;
|
||||
const {auth, requireEmailConfirmation} = this.props;
|
||||
const {emailVerificationLoading, emailVerificationSuccess} = auth;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!noButton && <Button id='coralSignInButton' onClick={showSignInDialog} full>
|
||||
Sign in to comment
|
||||
</Button>}
|
||||
<SignDialog
|
||||
open={auth.showSignInDialog}
|
||||
open={true}
|
||||
view={auth.view}
|
||||
offset={offset}
|
||||
emailVerificationEnabled={requireEmailConfirmation}
|
||||
emailVerificationLoading={emailVerificationLoading}
|
||||
emailVerificationSuccess={emailVerificationSuccess}
|
||||
@@ -189,7 +182,6 @@ const mapDispatchToProps = dispatch => ({
|
||||
fetchSignUpFacebook: () => dispatch(fetchSignUpFacebook()),
|
||||
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
|
||||
requestConfirmEmail: (email, url) => dispatch(requestConfirmEmail(email, url)),
|
||||
showSignInDialog: () => dispatch(showSignInDialog()),
|
||||
changeView: view => dispatch(changeView(view)),
|
||||
handleClose: () => dispatch(hideSignInDialog()),
|
||||
invalidForm: error => dispatch(invalidForm(error)),
|
||||
|
||||
+71
-37
@@ -53,6 +53,71 @@ const forEachField = (schema, fn) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorates the field with the post resolvers (if available) and attaches a
|
||||
* default type in the form of `Default${typeName}`.
|
||||
*/
|
||||
const decorateResolveFunction = (field, typeName, fieldName, post) => {
|
||||
|
||||
// Cache the original resolverType function.
|
||||
let resolveType = field.resolveType;
|
||||
|
||||
// defaultResolveType is the default type that is resolved on a resolver
|
||||
// when the interface being looked up is not defined.
|
||||
const defaultResolveType = `Default${typeName}`;
|
||||
|
||||
// Return the function to handle the resolveType hooks.
|
||||
const defaultResolveFn = (obj, context, info) => {
|
||||
let type = resolveType(obj, context, info);
|
||||
|
||||
// Only if a previous resolver was unable to resolve the field type do we
|
||||
// progress to the hooks (in order!) to resolve the field name until we
|
||||
// have resolved it.
|
||||
if (typeof type !== 'undefined' && type != null) {
|
||||
return type;
|
||||
}
|
||||
|
||||
// All else fails, resort to the defaultResolveType.
|
||||
return defaultResolveType;
|
||||
};
|
||||
|
||||
// This only needs to do something if post hooks are defined.
|
||||
if (post.length === 0) {
|
||||
|
||||
// Set the default on the resolveType function.
|
||||
field.resolveType = defaultResolveFn;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure it matches the format we expect.
|
||||
Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
|
||||
|
||||
// Return the function to handle the resolveType hooks.
|
||||
field.resolveType = (obj, context, info) => {
|
||||
let type = defaultResolveFn(obj, context, info);
|
||||
|
||||
// Only if a previous resolver was unable to resolve the field type do we
|
||||
// progress to the hooks (in order!) to resolve the field name until we
|
||||
// have resolved it.
|
||||
if (typeof type !== 'undefined' && type != null && type !== defaultResolveType) {
|
||||
return type;
|
||||
}
|
||||
|
||||
// We will walk through the post hooks until we find the right one. This
|
||||
// follows what redux does to combine existing reducers.
|
||||
for (let i = 0; i < post.length; i++) {
|
||||
let resolveType = post[i];
|
||||
let resolvedType = resolveType(obj, context, info);
|
||||
if (typeof resolvedType !== 'undefined' && resolvedType != null) {
|
||||
return resolvedType;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorates the schema with pre and post hooks as provided by the Plugin
|
||||
* Manager.
|
||||
@@ -115,11 +180,6 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
|
||||
post: []
|
||||
});
|
||||
|
||||
// If we have no hooks to add here, don't try to modify anything.
|
||||
if (pre.length === 0 && post.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a resolve type, we need to do some specific things to handle
|
||||
// this type of field.
|
||||
if (isResolveType) {
|
||||
@@ -129,39 +189,13 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
|
||||
throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`);
|
||||
}
|
||||
|
||||
// This only needs to do something if post hooks are defined.
|
||||
if (post.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure it matches the format we expect.
|
||||
Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
|
||||
|
||||
// Cache the original resolverType function.
|
||||
let resolveType = field.resolveType;
|
||||
|
||||
// Return the function to handle the resolveType hooks.
|
||||
field.resolveType = (obj, context, info) => {
|
||||
let type = resolveType(obj, context, info);
|
||||
|
||||
// Only if a previous resolver was unable to resolve the field type do we
|
||||
// progress to the hooks (in order!) to resolve the field name until we
|
||||
// have resolved it.
|
||||
if (typeof type !== 'undefined' && type != null) {
|
||||
return type;
|
||||
}
|
||||
|
||||
// We will walk through the post hooks until we find the right one. This
|
||||
// follows what redux does to combine existing reducers.
|
||||
for (let i = 0; i < post.length; i++) {
|
||||
let resolveType = post[i];
|
||||
type = resolveType(obj, context, info);
|
||||
if (typeof type !== 'undefined' && type != null) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Decorate the resolve function on the field with the new resolveType func.
|
||||
decorateResolveFunction(field, typeName, fieldName, post);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have no hooks to add here, don't try to modify anything.
|
||||
if (pre.length === 0 && post.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,14 @@ const Wordlist = require('../../services/wordlist');
|
||||
* @param {String} [status='NONE'] the status of the new comment
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => {
|
||||
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
|
||||
|
||||
let tags = [];
|
||||
// Building array of tags
|
||||
tags = tags.map(tag => ({name: tag}));
|
||||
|
||||
// If admin or moderator, adding STAFF tag
|
||||
if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) {
|
||||
tags = [{name: 'STAFF'}];
|
||||
tags.push({name: 'STAFF'});
|
||||
}
|
||||
|
||||
return CommentsService.publicCreate({
|
||||
|
||||
@@ -8,7 +8,7 @@ const ActionSummary = {
|
||||
case 'DONTAGREE':
|
||||
return 'DontAgreeActionSummary';
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ActionSummary;
|
||||
|
||||
@@ -2,8 +2,8 @@ const wrapResponse = require('../helpers/response');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
const RootMutation = {
|
||||
createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) {
|
||||
return wrapResponse('comment')(Comment.create({asset_id, parent_id, body}));
|
||||
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'}));
|
||||
|
||||
@@ -84,6 +84,9 @@ const RootQuery = {
|
||||
},
|
||||
|
||||
myIgnoredUsers: async (_, args, {user, loaders: {Users}}) => {
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get currentUser again since context.user was out of date when running test/graph/mutations/ignoreUser
|
||||
const currentUser = (await Users.getByQuery({ids: [user.id], limit: 1}))[0];
|
||||
|
||||
+60
-4
@@ -31,7 +31,7 @@ type User {
|
||||
username: String!
|
||||
|
||||
# Action summaries against the user.
|
||||
action_summaries: [ActionSummary]
|
||||
action_summaries: [ActionSummary]!
|
||||
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
@@ -197,7 +197,7 @@ type Comment {
|
||||
actions: [Action]
|
||||
|
||||
# Action summaries against a comment.
|
||||
action_summaries: [ActionSummary]
|
||||
action_summaries: [ActionSummary]!
|
||||
|
||||
# The asset that a comment was made on.
|
||||
asset: Asset
|
||||
@@ -229,6 +229,22 @@ interface Action {
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
# DefaultAction is the Action provided for undefined types.
|
||||
type DefaultAction 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
|
||||
}
|
||||
|
||||
# A summary of actions based on the specific grouping of the group_id.
|
||||
interface ActionSummary {
|
||||
|
||||
@@ -239,6 +255,16 @@ interface ActionSummary {
|
||||
current_user: Action
|
||||
}
|
||||
|
||||
# DefaultActionSummary is the ActionSummary provided for undefined types.
|
||||
type DefaultActionSummary implements ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: Action
|
||||
}
|
||||
|
||||
# A summary of actions for a specific action type on an Asset.
|
||||
interface AssetActionSummary {
|
||||
|
||||
@@ -249,6 +275,16 @@ interface AssetActionSummary {
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
# DefaultAssetActionSummary is the AssetActionSummary provided for undefined types.
|
||||
type DefaultAssetActionSummary implements AssetActionSummary {
|
||||
|
||||
# Number of actions associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the actions.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
# A summary of counts related to all the Flags on an Asset.
|
||||
type FlagAssetActionSummary implements AssetActionSummary {
|
||||
|
||||
@@ -440,7 +476,7 @@ type Asset {
|
||||
|
||||
# Summary of all Actions against all entities associated with the Asset.
|
||||
# (likes, flags, etc.). Requires the `ADMIN` role.
|
||||
action_summaries: [AssetActionSummary]
|
||||
action_summaries: [AssetActionSummary!]
|
||||
|
||||
# The date that the asset was created.
|
||||
created_at: Date
|
||||
@@ -602,6 +638,26 @@ input CreateLikeInput {
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
enum TAG_TYPE {
|
||||
STAFF
|
||||
}
|
||||
|
||||
input CreateCommentInput {
|
||||
|
||||
# The asset id
|
||||
asset_id: ID!
|
||||
|
||||
# The id of the parent comment
|
||||
parent_id: ID
|
||||
|
||||
# The body of the comment
|
||||
body: String!
|
||||
|
||||
# Tags
|
||||
tags: [TAG_TYPE]
|
||||
|
||||
}
|
||||
|
||||
type CreateLikeResponse implements Response {
|
||||
|
||||
# The like that was created.
|
||||
@@ -728,7 +784,7 @@ type StopIgnoringUserResponse implements Response {
|
||||
type RootMutation {
|
||||
|
||||
# Creates a comment on the asset.
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): CreateCommentResponse
|
||||
createComment(comment: CreateCommentInput!): CreateCommentResponse
|
||||
|
||||
# Creates a like on an entity.
|
||||
createLike(like: CreateLikeInput!): CreateLikeResponse
|
||||
|
||||
+1
-1
@@ -182,6 +182,6 @@
|
||||
"webpack": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^7.9.0"
|
||||
"node": "^7.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,38 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
|
||||
import styles from './styles.css';
|
||||
|
||||
class OffTopicCheckbox extends React.Component {
|
||||
|
||||
label = 'OFF_TOPIC';
|
||||
|
||||
handleChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
this.props.addTag(this.label)
|
||||
} else {
|
||||
const idx = this.props.commentBox.tags.indexOf(this.label);
|
||||
this.props.removeTag(idx);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.offTopic}>
|
||||
<label className={styles.offTopicLabel}>
|
||||
<input type="checkbox" onChange={this.handleChange}/>
|
||||
Off-Topic
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({addTag, removeTag}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const isOffTopic = (tags) => {
|
||||
return !!tags.filter(tag => tag.name === 'OFF_TOPIC').length
|
||||
}
|
||||
|
||||
export default (props) => (
|
||||
<span>
|
||||
{
|
||||
isOffTopic(props.comment.tags) ? (
|
||||
<span className={styles.tag}>
|
||||
Off-topic
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
</span>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
.offTopic {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.offTopicLabel {
|
||||
padding: 10px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: rgba(245, 188, 168, 0.6);
|
||||
display: inline-block;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import OffTopicCheckbox from './components/OffTopicCheckbox';
|
||||
import OffTopicTag from './components/OffTopicTag';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentBoxDetail: [OffTopicCheckbox],
|
||||
commentInfoBar: [OffTopicTag]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
const {readFileSync} = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8')
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
## Extending TAG_TYPE by adding OFF_TOPIC Tag
|
||||
enum TAG_TYPE {
|
||||
OFF_TOPIC
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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);
|
||||
|
||||
@@ -14,13 +15,11 @@ class RespectButton extends Component {
|
||||
const {postRespect, showSignInDialog, deleteAction, commentId} = this.props;
|
||||
const {me, comment} = this.props.data;
|
||||
|
||||
const respect = comment.action_summaries[0];
|
||||
const respected = (respect && respect.current_user);
|
||||
const myRespectActionSummary = getMyActionSummary('RespectActionSummary', comment);
|
||||
|
||||
// If the current user does not exist, trigger sign in dialog.
|
||||
if (!me) {
|
||||
const offset = document.getElementById(`c_${commentId}`).getBoundingClientRect().top - 75;
|
||||
showSignInDialog(offset);
|
||||
showSignInDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,29 +28,33 @@ class RespectButton extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!respected) {
|
||||
if (myRespectActionSummary) {
|
||||
deleteAction(myRespectActionSummary.current_user.id);
|
||||
} else {
|
||||
postRespect({
|
||||
item_id: commentId,
|
||||
item_type: 'COMMENTS'
|
||||
});
|
||||
} else {
|
||||
deleteAction(respect.current_user.id);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {comment} = this.props.data;
|
||||
const respect = comment && comment.action_summaries && comment.action_summaries[0];
|
||||
const respected = respect && respect.current_user;
|
||||
let count = respect ? respect.count : 0;
|
||||
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const myRespect = getMyActionSummary('RespectActionSummary', comment);
|
||||
let count = getTotalActionCount('RespectActionSummary', comment);
|
||||
|
||||
return (
|
||||
<div className={styles.respect}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.respected]: respected})}
|
||||
className={cn(styles.button, {[styles.respected]: myRespect})}
|
||||
onClick={this.handleClick} >
|
||||
<span>{lang.t(respected ? 'respected' : 'respect')}</span>
|
||||
<Icon className={cn(styles.icon, {[styles.respected]: respected})} />
|
||||
<span>{lang.t(myRespect ? 'respected' : 'respect')}</span>
|
||||
<Icon className={cn(styles.icon, {[styles.respected]: myRespect})} />
|
||||
{count > 0 && count}
|
||||
</button>
|
||||
</div>
|
||||
@@ -64,4 +67,3 @@ RespectButton.propTypes = {
|
||||
};
|
||||
|
||||
export default RespectButton;
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import RespectButton from '../components/RespectButton';
|
||||
// See https://dev-blog.apollodata.com/apollo-clients-new-imperative-store-api-6cb69318a1e3
|
||||
// and https://github.com/apollographql/apollo-client/issues/1224
|
||||
|
||||
const isRespectAction = (a) => a.__typename === 'RespectActionSummary';
|
||||
|
||||
export const RESPECT_QUERY = gql`
|
||||
query RespectQuery($commentId: ID!) {
|
||||
comment(id: $commentId) {
|
||||
@@ -52,18 +54,21 @@ const withDeleteAction = graphql(gql`
|
||||
},
|
||||
updateQueries: {
|
||||
RespectQuery: (prev) => {
|
||||
if (get(prev, 'comment.action_summaries.0.current_user.id') !== id) {
|
||||
const action_summaries = prev.comment.action_summaries;
|
||||
const idx = action_summaries.findIndex(isRespectAction);
|
||||
if (idx < 0 || get(action_summaries[idx], 'current_user.id') !== id) {
|
||||
return prev;
|
||||
}
|
||||
const next = {
|
||||
...prev,
|
||||
comment: {
|
||||
...prev.comment,
|
||||
action_summaries: [{
|
||||
__typename: 'RespectActionSummary',
|
||||
count: prev.comment.action_summaries[0].count - 1,
|
||||
current_user: null,
|
||||
}],
|
||||
action_summaries: action_summaries.map(
|
||||
(a, i) => i !== idx ? a : ({
|
||||
...a,
|
||||
count: a.count - 1,
|
||||
current_user: null,
|
||||
})),
|
||||
}
|
||||
};
|
||||
return next;
|
||||
@@ -102,21 +107,40 @@ const withPostRespect = graphql(gql`
|
||||
},
|
||||
updateQueries: {
|
||||
RespectQuery: (prev, {mutationResult, queryVariables}) => {
|
||||
if (queryVariables.commentId !== respect.item_id ||
|
||||
get(prev, 'comment.action_summaries.0.current_user')) {
|
||||
if (queryVariables.commentId !== respect.item_id) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
let action_summaries = prev.comment.action_summaries;
|
||||
let idx = action_summaries.findIndex(isRespectAction);
|
||||
|
||||
// Check whether we already respected this comment.
|
||||
if (idx >= 0 && action_summaries[idx].current_user) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
|
||||
// Add initial action when it doesn't exist.
|
||||
action_summaries = action_summaries.concat([{
|
||||
__typename: 'RespectActionSummary',
|
||||
count: 0,
|
||||
current_user: null,
|
||||
}]);
|
||||
idx = action_summaries.length - 1;
|
||||
}
|
||||
|
||||
const respectAction = mutationResult.data.createRespect.respect;
|
||||
const count = prev.comment.action_summaries[0] ? prev.comment.action_summaries[0].count : 0;
|
||||
const next = {
|
||||
...prev,
|
||||
comment: {
|
||||
...prev.comment,
|
||||
action_summaries: [{
|
||||
__typename: 'RespectActionSummary',
|
||||
count: count + 1,
|
||||
current_user: respectAction,
|
||||
}],
|
||||
action_summaries: action_summaries.map(
|
||||
(a, i) => i !== idx ? a : ({
|
||||
...a,
|
||||
count: a.count + 1,
|
||||
current_user: respectAction,
|
||||
})),
|
||||
}
|
||||
};
|
||||
return next;
|
||||
@@ -138,4 +162,3 @@ const enhance = compose(
|
||||
);
|
||||
|
||||
export default enhance(RespectButton);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import RespectButton from './containers/RespectButton';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentDetail: [RespectButton],
|
||||
|
||||
+8
-2
@@ -48,10 +48,16 @@ module.exports = class ActionsService {
|
||||
* Finds actions in an array of ids.
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
static findByItemIdArray(item_ids) {
|
||||
return ActionModel.find({
|
||||
static async findByItemIdArray(item_ids) {
|
||||
let actions = await ActionModel.find({
|
||||
'item_id': {$in: item_ids}
|
||||
});
|
||||
|
||||
if (actions === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,10 +3,10 @@ const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const ActionsService = require('./actions');
|
||||
|
||||
const ALLOWED_TAGS = [
|
||||
{name: 'STAFF'},
|
||||
{name: 'BEST'},
|
||||
];
|
||||
// const ALLOWED_TAGS = [
|
||||
// {name: 'STAFF'},
|
||||
// {name: 'BEST'},
|
||||
// ];
|
||||
|
||||
const STATUSES = [
|
||||
'ACCEPTED',
|
||||
@@ -53,9 +53,10 @@ module.exports = class CommentsService {
|
||||
*/
|
||||
static addTag(id, name, assigned_by) {
|
||||
|
||||
if (ALLOWED_TAGS.find((t) => t.name === name) == null) {
|
||||
return Promise.reject(new Error('tag not allowed'));
|
||||
}
|
||||
// Disabling allowed tags until we are able to extend them
|
||||
// if (ALLOWED_TAGS.find((t) => t.name === name) == null) {
|
||||
// return Promise.reject(new Error('tag not allowed'));
|
||||
// }
|
||||
|
||||
const filter = {
|
||||
id,
|
||||
|
||||
@@ -12,8 +12,8 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => SettingsService.init());
|
||||
|
||||
const query = `
|
||||
mutation CreateComment($body: String = "Here's my comment!") {
|
||||
createComment(asset_id: "123", body: $body) {
|
||||
mutation CreateComment($comment: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) {
|
||||
createComment(comment: $comment) {
|
||||
comment {
|
||||
id
|
||||
status
|
||||
@@ -170,7 +170,10 @@ describe('graph.mutations.createComment', () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
body
|
||||
comment: {
|
||||
asset_id: '123',
|
||||
body
|
||||
}
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const expect = require('chai').expect;
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../graph/schema');
|
||||
const Context = require('../../../graph/context');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UsersService = require('../../../../services/users');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
|
||||
const ignoreUserMutation = `
|
||||
mutation ignoreUser ($id: ID!) {
|
||||
@@ -94,7 +94,7 @@ describe('graph.mutations.stopIgnoringUser', () => {
|
||||
if (response.errors && response.errors.length) {
|
||||
console.error(response.errors);
|
||||
}
|
||||
expect(response.errors).to.be.empty;
|
||||
expect(response.errors).to.be.empty;
|
||||
});
|
||||
|
||||
const stopIgnoringUserMutation = `
|
||||
@@ -112,7 +112,7 @@ describe('graph.mutations.stopIgnoringUser', () => {
|
||||
if (stopIgnoringUserResponse.errors && stopIgnoringUserResponse.errors.length) {
|
||||
console.error(stopIgnoringUserResponse.errors);
|
||||
}
|
||||
expect(stopIgnoringUserResponse.errors).to.be.empty;
|
||||
expect(stopIgnoringUserResponse.errors).to.be.empty;
|
||||
|
||||
// now check my ignored users
|
||||
const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {});
|
||||
@@ -1,12 +1,12 @@
|
||||
const expect = require('chai').expect;
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../graph/schema');
|
||||
const Context = require('../../../graph/context');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const Asset = require('../../../models/asset');
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UsersService = require('../../../../services/users');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const Asset = require('../../../../models/asset');
|
||||
const CommentsService = require('../../../../services/comments');
|
||||
|
||||
describe('graph.queries.asset', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -87,7 +87,7 @@ describe('graph.queries.asset', () => {
|
||||
`;
|
||||
const assetCommentsResponse = await graphql(schema, assetCommentsWithoutIgnoredQuery, {}, context, {assetId, assetUrl, excludeIgnored: true});
|
||||
const comments = assetCommentsResponse.data.asset.comments;
|
||||
expect(comments.length).to.equal(2);
|
||||
expect(comments.length).to.equal(2);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta property="csrf" content="<%= csrfToken %>">
|
||||
<link rel="stylesheet" type="text/css" href="/client/embed/stream/default.css">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
Reference in New Issue
Block a user