mirror of
https://github.com/wassname/talk.git
synced 2026-07-21 12:51:03 +08:00
Merge branch 'master' into locale-local-storage
This commit is contained in:
@@ -4,7 +4,7 @@ import { CSSTransitionGroup } from 'react-transition-group';
|
||||
import styles from './CommentAnimatedEdit.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CommentBodyHighlighter = ({ children, body }) => {
|
||||
const CommentAnimatedEdit = ({ children, body }) => {
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
@@ -27,9 +27,9 @@ const CommentBodyHighlighter = ({ children, body }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CommentBodyHighlighter.propTypes = {
|
||||
CommentAnimatedEdit.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentBodyHighlighter;
|
||||
export default CommentAnimatedEdit;
|
||||
|
||||
+43
-11
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { matchLinks } from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
@@ -62,16 +63,47 @@ function markLinks(body) {
|
||||
return content;
|
||||
}
|
||||
|
||||
export default ({ suspectWords, bannedWords, body, ...rest }) => {
|
||||
// First highlight links.
|
||||
const content = markLinks(body).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
const CommentFormatter = ({
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
className = 'comment',
|
||||
...rest
|
||||
}) => {
|
||||
// Breaking the body by line break
|
||||
const textbreaks = body.split('\n');
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
return <div {...rest}>{content}</div>;
|
||||
return (
|
||||
<span className={`${className}-text`} {...rest}>
|
||||
{textbreaks.map((line, i) => {
|
||||
const content = markLinks(line).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
|
||||
return (
|
||||
<span key={i} className={`${className}-line`}>
|
||||
{content}
|
||||
{i !== textbreaks.length - 1 && (
|
||||
<br className={`${className}-linebreak`} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentFormatter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array,
|
||||
suspectWords: PropTypes.array,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentFormatter;
|
||||
@@ -5,7 +5,7 @@ import { Link } from 'react-router';
|
||||
import { Icon } from 'coral-ui';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
@@ -78,11 +78,12 @@ class UserDetailComment extends React.Component {
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
<CommentFormatter
|
||||
suspectWords={suspect}
|
||||
bannedWords={banned}
|
||||
body={comment.body}
|
||||
/>{' '}
|
||||
className="talk-admin-user-detail-comment"
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
|
||||
@@ -8,7 +8,7 @@ import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
@@ -126,11 +126,12 @@ class Comment extends React.Component {
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
<CommentFormatter
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
className="talk-admin-comment"
|
||||
body={comment.body}
|
||||
/>{' '}
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
|
||||
@@ -265,6 +265,15 @@ const ErrCannotIgnoreStaff = new APIError('Cannot ignore staff members.', {
|
||||
status: 400,
|
||||
});
|
||||
|
||||
// ErrParentDoesNotVisible is returned when the user tries to reply to a comment
|
||||
// that isn't visible.
|
||||
const ErrParentDoesNotVisible = new APIError(
|
||||
'Cannot reply to a comment that is not visible',
|
||||
{
|
||||
translation_key: 'COMMENT_PARENT_NOT_VISIBLE',
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
APIError,
|
||||
ErrAlreadyExists,
|
||||
@@ -276,24 +285,25 @@ module.exports = {
|
||||
ErrContainsProfanity,
|
||||
ErrEditWindowHasEnded,
|
||||
ErrEmailTaken,
|
||||
ErrEmailVerificationToken,
|
||||
ErrInstallLock,
|
||||
ErrInvalidAssetURL,
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
ErrMaxRateLimit,
|
||||
ErrMissingEmail,
|
||||
ErrMissingPassword,
|
||||
ErrEmailVerificationToken,
|
||||
ErrPasswordResetToken,
|
||||
ErrMissingUsername,
|
||||
ErrNotAuthorized,
|
||||
ErrNotFound,
|
||||
ErrNotVerified,
|
||||
ErrParentDoesNotVisible,
|
||||
ErrPasswordResetToken,
|
||||
ErrPasswordTooShort,
|
||||
ErrPermissionUpdateUsername,
|
||||
ErrSameUsernameProvided,
|
||||
ErrSettingsInit,
|
||||
ErrSettingsNotInit,
|
||||
ErrSpecialChars,
|
||||
ErrUsernameTaken,
|
||||
ErrSameUsernameProvided,
|
||||
ExtendableError,
|
||||
};
|
||||
|
||||
@@ -198,6 +198,7 @@ en:
|
||||
embedlink:
|
||||
copy: "Copy to Clipboard"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
|
||||
COMMENT_TOO_SHORT: "Comments should be more than one character, please revise your comment and try again."
|
||||
|
||||
@@ -182,6 +182,7 @@ es:
|
||||
embedlink:
|
||||
copy: "Copiar al portapapeles"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe."
|
||||
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
|
||||
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
|
||||
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
|
||||
|
||||
@@ -37,6 +37,17 @@ const ActionSchema = new Schema(
|
||||
}
|
||||
);
|
||||
|
||||
// Create an index on the `item_id` field so that queries looking for
|
||||
// actions based on the item id can resolve faster.
|
||||
ActionSchema.index(
|
||||
{
|
||||
item_id: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
const Action = mongoose.model('Action', ActionSchema);
|
||||
|
||||
module.exports = Action;
|
||||
|
||||
+9
-10
@@ -68,17 +68,16 @@ then
|
||||
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
fi
|
||||
|
||||
if [ "$CIRCLE_BRANCH" = "master" ]
|
||||
# deploy based on the env
|
||||
if [ -n "$CIRCLE_TAG" ]
|
||||
then
|
||||
|
||||
# deploy based on the env
|
||||
if [ -n "$CIRCLE_TAG" ]
|
||||
then
|
||||
deploy_tag
|
||||
else
|
||||
deploy_latest
|
||||
fi
|
||||
deploy_tag
|
||||
else
|
||||
deploy_branch
|
||||
if [ "$CIRCLE_BRANCH" = "master" ]
|
||||
then
|
||||
deploy_latest
|
||||
else
|
||||
deploy_branch
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
+20
-15
@@ -7,24 +7,31 @@ const SettingsService = require('./settings');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
const errors = require('../errors');
|
||||
const events = require('./events');
|
||||
const merge = require('lodash/merge');
|
||||
const { COMMENTS_NEW, COMMENTS_EDIT } = require('./events/constants');
|
||||
|
||||
module.exports = class CommentsService {
|
||||
/**
|
||||
* Creates a new Comment that came from a public source.
|
||||
* @param {Mixed} comment either a single comment or an array of comments.
|
||||
* @param {Object} input either a single comment or an array of comments.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async publicCreate(comment) {
|
||||
// Check to see if this is an array of comments, if so map it out.
|
||||
if (Array.isArray(comment)) {
|
||||
return Promise.all(comment.map(CommentsService.publicCreate));
|
||||
static async publicCreate(input) {
|
||||
// Extract the parent_id from the comment, if there is one.
|
||||
const { status = 'NONE', parent_id = null } = input;
|
||||
|
||||
// Check to see if we are replying to a comment, and if that comment is
|
||||
// visible.
|
||||
if (parent_id !== null) {
|
||||
const parent = await CommentModel.findOne({ id: parent_id });
|
||||
if (parent === null || !parent.visible) {
|
||||
throw errors.ErrParentDoesNotVisible;
|
||||
}
|
||||
}
|
||||
|
||||
const { status = 'NONE' } = comment;
|
||||
|
||||
const commentModel = new CommentModel(
|
||||
Object.assign(
|
||||
// Create the comment in the database.
|
||||
const comment = await CommentModel.create(
|
||||
merge(
|
||||
{
|
||||
status_history: status
|
||||
? [
|
||||
@@ -36,21 +43,19 @@ module.exports = class CommentsService {
|
||||
: [],
|
||||
body_history: [
|
||||
{
|
||||
body: comment.body,
|
||||
body: input.body,
|
||||
created_at: new Date(),
|
||||
},
|
||||
],
|
||||
},
|
||||
comment
|
||||
input
|
||||
)
|
||||
);
|
||||
|
||||
const savedCommentModel = await commentModel.save();
|
||||
|
||||
// Emit that the comment was created!
|
||||
await events.emitAsync(COMMENTS_NEW, savedCommentModel);
|
||||
await events.emitAsync(COMMENTS_NEW, comment);
|
||||
|
||||
return savedCommentModel;
|
||||
return comment;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -7,6 +7,6 @@
|
||||
],
|
||||
"extends": "../.eslintrc.json",
|
||||
"rules": {
|
||||
"mocha/no-exclusive-tests": "warn"
|
||||
"mocha/no-exclusive-tests": "error"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ describe('graph.mutations.createComment', () => {
|
||||
}
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment.comment).to.have.property('id').not
|
||||
.null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -34,12 +34,14 @@ describe('graph.queries.asset', () => {
|
||||
username: 'usernameC',
|
||||
},
|
||||
]);
|
||||
comments = await CommentsService.publicCreate(
|
||||
[0, 0, 1, 1].map(idx => ({
|
||||
author_id: users[idx].id,
|
||||
asset_id: assets[idx].id,
|
||||
body: `hello there! ${String(Math.random()).slice(2)}`,
|
||||
}))
|
||||
comments = await Promise.all(
|
||||
[0, 0, 1, 1].map(idx =>
|
||||
CommentsService.publicCreate({
|
||||
author_id: users[idx].id,
|
||||
asset_id: assets[idx].id,
|
||||
body: `hello there! ${String(Math.random()).slice(2)}`,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -176,28 +176,30 @@ describe('services.AssetsService', () => {
|
||||
);
|
||||
|
||||
// Create some comments on both assets.
|
||||
await CommentsService.publicCreate([
|
||||
{
|
||||
asset_id: '1',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
asset_id: '1',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
asset_id: '2',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
asset_id: '2',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
]);
|
||||
await Promise.all(
|
||||
[
|
||||
{
|
||||
asset_id: '1',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
asset_id: '1',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
asset_id: '2',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
asset_id: '2',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
].map(comment => CommentsService.publicCreate(comment))
|
||||
);
|
||||
|
||||
// Merge all the comments from asset 1 into asset 2, followed by deleting
|
||||
// asset 1.
|
||||
|
||||
@@ -131,6 +131,64 @@ describe('services.CommentsService', () => {
|
||||
});
|
||||
|
||||
describe('#publicCreate()', () => {
|
||||
describe('does not allow replies to comments that are not visible', () => {
|
||||
it('parent not found', async () => {
|
||||
try {
|
||||
await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
parent_id: 'does not exist',
|
||||
});
|
||||
throw new Error('comment should not have been created');
|
||||
} catch (err) {
|
||||
expect(err).to.have.property(
|
||||
'translation_key',
|
||||
'COMMENT_PARENT_NOT_VISIBLE'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('parent REJECTED', async () => {
|
||||
try {
|
||||
const parent = await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'REJECTED',
|
||||
});
|
||||
await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
parent_id: parent.id,
|
||||
});
|
||||
throw new Error('comment should not have been created');
|
||||
} catch (err) {
|
||||
expect(err).to.have.property(
|
||||
'translation_key',
|
||||
'COMMENT_PARENT_NOT_VISIBLE'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('parent SYSTEM_WITHHELD', async () => {
|
||||
try {
|
||||
const parent = await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'SYSTEM_WITHHELD',
|
||||
});
|
||||
await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
parent_id: parent.id,
|
||||
});
|
||||
throw new Error('comment should not have been created');
|
||||
} catch (err) {
|
||||
expect(err).to.have.property(
|
||||
'translation_key',
|
||||
'COMMENT_PARENT_NOT_VISIBLE'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a new comment', async () => {
|
||||
const c = await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
@@ -142,31 +200,6 @@ describe('services.CommentsService', () => {
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.be.equal('ACCEPTED');
|
||||
});
|
||||
|
||||
it('creates many new comments', async () => {
|
||||
const [c1, c2, c3] = await CommentsService.publicCreate([
|
||||
{
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
{
|
||||
body: 'This is another comment!',
|
||||
},
|
||||
{
|
||||
body: 'This is a rejected comment!',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.status).to.be.equal('ACCEPTED');
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.status).to.be.equal('NONE');
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.status).to.be.equal('REJECTED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#edit', () => {
|
||||
|
||||
Reference in New Issue
Block a user