charCount ? `${name}-char-max` : ''}`}>
- {
- charCount &&
- `${charCount - length} ${lang.t('characters-remaining')}`
- }
+ {charCount && `${charCount - length} ${lang.t('characters-remaining')}`}
+
{
isReply && (
{
- cancelButtonClicked('');
- }}>
+ onClick={() => cancelButtonClicked('')}>
{lang.t('cancel')}
)
}
{ authorId && (
charCount) ? 'lightGrey' : 'darkGrey'}
+ cStyle={enablePostComment ? 'lightGrey' : 'darkGrey'}
className={`${name}-button`}
- onClick={this.postComment}>
+ onClick={this.postComment}
+ disabled={enablePostComment ? 'disabled' : ''}>
{lang.t('post')}
)
@@ -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);
diff --git a/client/coral-plugin-commentbox/actions.js b/client/coral-plugin-commentbox/actions.js
new file mode 100644
index 000000000..46bcf4b1b
--- /dev/null
+++ b/client/coral-plugin-commentbox/actions.js
@@ -0,0 +1,9 @@
+export const addTag = tag => ({
+ type: 'ADD_TAG',
+ tag
+});
+
+export const removeTag = idx => ({
+ type: 'REMOVE_TAG',
+ idx
+});
diff --git a/client/coral-plugin-commentbox/constants.js b/client/coral-plugin-commentbox/constants.js
new file mode 100644
index 000000000..b1290f02e
--- /dev/null
+++ b/client/coral-plugin-commentbox/constants.js
@@ -0,0 +1,2 @@
+export const ADD_TAG = 'ADD_TAG';
+export const REMOVE_TAG = 'REMOVE_TAG';
diff --git a/client/coral-plugin-commentbox/index.js b/client/coral-plugin-commentbox/index.js
new file mode 100644
index 000000000..9f166caeb
--- /dev/null
+++ b/client/coral-plugin-commentbox/index.js
@@ -0,0 +1,5 @@
+import reducer from './reducer';
+
+export default {
+ reducer
+};
diff --git a/client/coral-plugin-commentbox/reducer.js b/client/coral-plugin-commentbox/reducer.js
new file mode 100644
index 000000000..d9065fe87
--- /dev/null
+++ b/client/coral-plugin-commentbox/reducer.js
@@ -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;
+ }
+}
diff --git a/client/coral-plugin-commentbox/styles.css b/client/coral-plugin-commentbox/styles.css
new file mode 100644
index 000000000..d0f851f18
--- /dev/null
+++ b/client/coral-plugin-commentbox/styles.css
@@ -0,0 +1,6 @@
+.slot {
+ display: inline-block;
+ div {
+ display: inline-block;
+ }
+}
diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js
index 45110f407..16e616379 100644
--- a/client/coral-plugin-flags/FlagButton.js
+++ b/client/coral-plugin-flags/FlagButton.js
@@ -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
diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js
index 51854c57f..28382dbc2 100644
--- a/client/coral-plugin-likes/LikeButton.js
+++ b/client/coral-plugin-likes/LikeButton.js
@@ -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) {
diff --git a/client/coral-settings/components/NotLoggedIn.js b/client/coral-settings/components/NotLoggedIn.js
index c76553c50..a5d8b0693 100644
--- a/client/coral-settings/components/NotLoggedIn.js
+++ b/client/coral-settings/components/NotLoggedIn.js
@@ -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}) => (
-
{lang.t('fromSettingsPage')}
diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js
index 2d756077d..553504462 100644
--- a/client/coral-settings/containers/ProfileContainer.js
+++ b/client/coral-settings/containers/ProfileContainer.js
@@ -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
;
- }
-
if (data.loading) {
return ;
}
+ if (!loggedIn || !me) {
+ return ;
+ }
+
const localProfile = this.props.user.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
@@ -81,7 +84,6 @@ class ProfileContainer extends Component {
:
{lang.t('userNoComment')}
}
-
);
}
@@ -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),
diff --git a/client/coral-sign-in/components/CreateUsernameDialog.js b/client/coral-sign-in/components/CreateUsernameDialog.js
index 4a7a99b81..b94b47c42 100644
--- a/client/coral-sign-in/components/CreateUsernameDialog.js
+++ b/client/coral-sign-in/components/CreateUsernameDialog.js
@@ -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 (
+ open={open}>
×
@@ -42,6 +38,7 @@ const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmit
this.emailInput = input}
type="text"
+ style={{fontSize: 16}}
id="email"
name="email" />
diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js
index 6243472b4..ff2464f7c 100644
--- a/client/coral-sign-in/components/SignDialog.js
+++ b/client/coral-sign-in/components/SignDialog.js
@@ -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}) => (
+ open={open}>
×
{view === 'SIGNIN' && }
{view === 'SIGNUP' && }
diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js
index a0ed5b5b4..7460efb97 100644
--- a/client/coral-sign-in/components/SignInContent.js
+++ b/client/coral-sign-in/components/SignInContent.js
@@ -20,8 +20,8 @@ const SignInContent = ({
}) => {
return (
-
-
+
+
{auth.emailVerificationFailure ? lang.t('signIn.emailVerifyCTA') : lang.t('signIn.signIn')}
@@ -42,7 +42,7 @@ const SignInContent = ({
{emailVerificationSuccess &&
}
:
-
+
{lang.t('signIn.facebookSignIn')}
@@ -58,6 +58,7 @@ const SignInContent = ({
type="email"
label={lang.t('signIn.email')}
value={formData.email}
+ style={{fontSize: 16}}
onChange={handleChange}
/>
@@ -80,7 +82,7 @@ const SignInContent = ({
}
-
+
changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}
{lang.t('signIn.needAnAccount')}
diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js
index d127cc2c7..921f045be 100644
--- a/client/coral-sign-in/components/SignUpContent.js
+++ b/client/coral-sign-in/components/SignUpContent.js
@@ -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}
diff --git a/client/coral-sign-in/containers/ChangeUsernameContainer.js b/client/coral-sign-in/containers/ChangeUsernameContainer.js
index c4450aa56..a3b4b969d 100644
--- a/client/coral-sign-in/containers/ChangeUsernameContainer.js
+++ b/client/coral-sign-in/containers/ChangeUsernameContainer.js
@@ -100,12 +100,11 @@ class ChangeUsernameContainer extends Component {
}
render() {
- const {loggedIn, auth, offset} = this.props;
+ const {loggedIn, auth} = this.props;
return (
- {!noButton &&
- Sign in to comment
- }
({
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)),
diff --git a/graph/hooks.js b/graph/hooks.js
index c5b7541b3..7f6be7feb 100644
--- a/graph/hooks.js
+++ b/graph/hooks.js
@@ -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;
}
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index 728603ae2..982f7ade7 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -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({
diff --git a/graph/resolvers/action_summary.js b/graph/resolvers/action_summary.js
index 1986a0648..ac2154de8 100644
--- a/graph/resolvers/action_summary.js
+++ b/graph/resolvers/action_summary.js
@@ -8,7 +8,7 @@ const ActionSummary = {
case 'DONTAGREE':
return 'DontAgreeActionSummary';
}
- },
+ }
};
module.exports = ActionSummary;
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index 38183bff6..661cc9f96 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -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'}));
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index b26ecabca..4dcb7ea11 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -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];
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index ef6ff5937..32c2c16c2 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -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
diff --git a/out.gif b/out.gif
new file mode 100644
index 000000000..e69de29bb
diff --git a/package.json b/package.json
index 1b09c78c8..6c725a852 100644
--- a/package.json
+++ b/package.json
@@ -182,6 +182,6 @@
"webpack": "^2.3.1"
},
"engines": {
- "node": "^7.9.0"
+ "node": "^7.8.0"
}
}
diff --git a/plugins/coral-plugin-offtopic/client/.babelrc b/plugins/coral-plugin-offtopic/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/client/.babelrc
@@ -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"
+ ]
+}
\ No newline at end of file
diff --git a/plugins/coral-plugin-offtopic/client/.eslintrc.json b/plugins/coral-plugin-offtopic/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/client/.eslintrc.json
@@ -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"] }]
+ }
+}
diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js
new file mode 100644
index 000000000..4e7ef7adf
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js
@@ -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 (
+
+
+
+ Off-Topic
+
+
+ )
+ }
+}
+
+
+const mapStateToProps = ({commentBox}) => ({commentBox});
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators({addTag, removeTag}, dispatch);
+
+export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js
new file mode 100644
index 000000000..73e2372ed
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js
@@ -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) => (
+
+ {
+ isOffTopic(props.comment.tags) ? (
+
+ Off-topic
+
+ ) : null
+ }
+
+);
diff --git a/plugins/coral-plugin-offtopic/client/components/styles.css b/plugins/coral-plugin-offtopic/client/components/styles.css
new file mode 100644
index 000000000..f3e290b09
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/client/components/styles.css
@@ -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;
+}
+
+
diff --git a/plugins/coral-plugin-offtopic/client/index.js b/plugins/coral-plugin-offtopic/client/index.js
new file mode 100644
index 000000000..c006c9a9e
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/client/index.js
@@ -0,0 +1,9 @@
+import OffTopicCheckbox from './components/OffTopicCheckbox';
+import OffTopicTag from './components/OffTopicTag';
+
+export default {
+ slots: {
+ commentBoxDetail: [OffTopicCheckbox],
+ commentInfoBar: [OffTopicTag]
+ }
+};
diff --git a/plugins/coral-plugin-offtopic/index.js b/plugins/coral-plugin-offtopic/index.js
new file mode 100644
index 000000000..51ad1c24e
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/index.js
@@ -0,0 +1,6 @@
+const {readFileSync} = require('fs');
+const path = require('path');
+
+module.exports = {
+ typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8')
+};
diff --git a/plugins/coral-plugin-offtopic/server/typeDefs.graphql b/plugins/coral-plugin-offtopic/server/typeDefs.graphql
new file mode 100644
index 000000000..48ba577cf
--- /dev/null
+++ b/plugins/coral-plugin-offtopic/server/typeDefs.graphql
@@ -0,0 +1,4 @@
+## Extending TAG_TYPE by adding OFF_TOPIC Tag
+enum TAG_TYPE {
+ OFF_TOPIC
+}
diff --git a/plugins/coral-plugin-respect/client/components/RespectButton.js b/plugins/coral-plugin-respect/client/components/RespectButton.js
index 9e851460e..c1e0c1b47 100644
--- a/plugins/coral-plugin-respect/client/components/RespectButton.js
+++ b/plugins/coral-plugin-respect/client/components/RespectButton.js
@@ -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 (
- {lang.t(respected ? 'respected' : 'respect')}
-
+ {lang.t(myRespect ? 'respected' : 'respect')}
+
{count > 0 && count}
@@ -64,4 +67,3 @@ RespectButton.propTypes = {
};
export default RespectButton;
-
diff --git a/plugins/coral-plugin-respect/client/containers/RespectButton.js b/plugins/coral-plugin-respect/client/containers/RespectButton.js
index 047f7991f..bcf0e342d 100644
--- a/plugins/coral-plugin-respect/client/containers/RespectButton.js
+++ b/plugins/coral-plugin-respect/client/containers/RespectButton.js
@@ -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);
-
diff --git a/plugins/coral-plugin-respect/client/index.js b/plugins/coral-plugin-respect/client/index.js
index 2caa6611b..a336786d6 100644
--- a/plugins/coral-plugin-respect/client/index.js
+++ b/plugins/coral-plugin-respect/client/index.js
@@ -1,4 +1,5 @@
import RespectButton from './containers/RespectButton';
+
export default {
slots: {
commentDetail: [RespectButton],
diff --git a/services/actions.js b/services/actions.js
index 0bcb81af0..40bef65bb 100644
--- a/services/actions.js
+++ b/services/actions.js
@@ -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;
}
/**
diff --git a/services/comments.js b/services/comments.js
index 75b53873a..7e9cca20f 100644
--- a/services/comments.js
+++ b/services/comments.js
@@ -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,
diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js
index b3bdd459a..cf8481094 100644
--- a/test/server/graph/mutations/createComment.js
+++ b/test/server/graph/mutations/createComment.js
@@ -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;
diff --git a/test/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js
similarity index 93%
rename from test/graph/mutations/ignoreUser.js
rename to test/server/graph/mutations/ignoreUser.js
index c5633bfb4..54d6305a1 100644
--- a/test/graph/mutations/ignoreUser.js
+++ b/test/server/graph/mutations/ignoreUser.js
@@ -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, {});
diff --git a/test/graph/queries/asset.js b/test/server/graph/queries/asset.js
similarity index 88%
rename from test/graph/queries/asset.js
rename to test/server/graph/queries/asset.js
index ba31fe330..79abeda0e 100644
--- a/test/graph/queries/asset.js
+++ b/test/server/graph/queries/asset.js
@@ -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);
});
});
diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs
index 498090e32..d99d4bce1 100644
--- a/views/embed/stream.ejs
+++ b/views/embed/stream.ejs
@@ -1,6 +1,8 @@
+
+