mirror of
https://github.com/wassname/talk.git
synced 2026-07-16 11:22:16 +08:00
Merge branch 'master' into performance-enhancements
This commit is contained in:
@@ -119,6 +119,8 @@
|
||||
|
||||
.commentInfoBar {
|
||||
margin-left: auto;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@keyframes enter {
|
||||
@@ -131,9 +133,6 @@
|
||||
animation: enter 1000ms;
|
||||
}
|
||||
|
||||
.commentContainer {
|
||||
}
|
||||
|
||||
.commentAvatar {
|
||||
max-width: 60px;
|
||||
}
|
||||
@@ -152,9 +151,32 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.tagsContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tagsContainer > * {
|
||||
margin: 3px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -452,39 +452,43 @@ export default class Comment extends React.Component {
|
||||
<div className={cn(styles.commentContainer, 'talk-stream-comment-container')}>
|
||||
<div className={cn(styles.header, 'talk-stream-comment-header')}>
|
||||
|
||||
<Slot
|
||||
className={cn(styles.username, 'talk-stream-comment-user-name')}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
<Slot
|
||||
className={cn('talk-stream-comment-author-tags')}
|
||||
fill="commentAuthorTags"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
inline
|
||||
/>
|
||||
|
||||
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
|
||||
<div className={cn(styles.headerContainer, 'talk-stream-comment-header-container')}>
|
||||
<Slot
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
className={cn(styles.username, 'talk-stream-comment-user-name')}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
|
||||
<div className={cn(styles.tagsContainer, 'talk-stream-comment-header-tags-container')}>
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
<Slot
|
||||
className={cn(styles.commentAuthorTagsSlot, 'talk-stream-comment-author-tags')}
|
||||
fill="commentAuthorTags"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
|
||||
<Slot
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
className={styles.commentInfoBar}
|
||||
|
||||
@@ -190,11 +190,9 @@ body {
|
||||
background-color: #4C1066;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 5px 6px;
|
||||
}
|
||||
|
||||
/* Comment Action Styles */
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
display: inline-block;
|
||||
color: #696969;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,12 @@ const CONFIG = {
|
||||
DISABLE_AUTOFLAG_SUSPECT_WORDS: process.env.TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS === 'TRUE',
|
||||
|
||||
// TRUST_THRESHOLDS defines the thresholds used for automoderation.
|
||||
TRUST_THRESHOLDS: process.env.TRUST_THRESHOLDS || 'comment:2,-1;flag:2,-1'
|
||||
TRUST_THRESHOLDS: process.env.TRUST_THRESHOLDS || 'comment:2,-1;flag:2,-1',
|
||||
|
||||
// IGNORE_FLAGS_AGAINST_STAFF disables staff members from entering the
|
||||
// reported queue from comments after this was enabled and from reports
|
||||
// against the staff members user account.
|
||||
IGNORE_FLAGS_AGAINST_STAFF: process.env.TALK_DISABLE_IGNORE_FLAGS_AGAINST_STAFF === 'TRUE',
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -456,3 +456,8 @@ Could be read as:
|
||||
added back to the queue.
|
||||
- At the moment of writing, behavior is not attached to the flagging
|
||||
reliability, but it is recorded.
|
||||
|
||||
## TALK_DISABLE_IGNORE_FLAGS_AGAINST_STAFF
|
||||
|
||||
When `TRUE`, staff members will have their accounts and comments moderated the
|
||||
same as any other user in the system. (Default `FALSE`)
|
||||
+89
-28
@@ -1,28 +1,80 @@
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
const UsersService = require('../../services/users');
|
||||
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
|
||||
const {
|
||||
IGNORE_FLAGS_AGAINST_STAFF,
|
||||
} = require('../../config');
|
||||
|
||||
/**
|
||||
* getActionItem will return the item that is associated with the given action.
|
||||
* If it does not exist, it will throw an error.
|
||||
*
|
||||
* @param {Object} ctx the graphql context for the request
|
||||
* @param {Object} action the action being performed
|
||||
* @return {Promise} resolves to the referenced item
|
||||
*/
|
||||
const getActionItem = async (ctx, {item_id, item_type}) => {
|
||||
const {
|
||||
loaders: {
|
||||
Comments,
|
||||
Users,
|
||||
},
|
||||
} = ctx;
|
||||
|
||||
if (item_type === 'COMMENTS') {
|
||||
const comment = await Comments.get.load(item_id);
|
||||
if (!comment) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
return comment;
|
||||
} else if (item_type === 'USERS') {
|
||||
const user = await Users.getByID.load(item_id);
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action on a item. If the item is a user flag, sets the user's status to
|
||||
* pending.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} item_id id of the item to add the action to
|
||||
* @param {String} item_type type of the item
|
||||
* @param {String} action_type type of the action
|
||||
* @return {Promise} resolves to the action created
|
||||
*
|
||||
* @param {Object} ctx the graphql context for the request
|
||||
* @param {Object} action the action being created
|
||||
* @return {Promise} resolves to the action created
|
||||
*/
|
||||
const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
|
||||
const createAction = async (ctx, {item_id, item_type, action_type, group_id, metadata = {}}) => {
|
||||
const {
|
||||
user = {},
|
||||
pubsub,
|
||||
connectors: {
|
||||
services: {
|
||||
Actions,
|
||||
},
|
||||
},
|
||||
} = ctx;
|
||||
|
||||
let comment;
|
||||
if (item_type === 'COMMENTS') {
|
||||
comment = await Comments.get.load(item_id);
|
||||
if (!comment) {
|
||||
throw new Error('Comment not found');
|
||||
// Gets the item referenced by the action.
|
||||
const item = await getActionItem(ctx, {item_id, item_type});
|
||||
|
||||
// If we are ignoring flags against staff, ensure that the target isn't a
|
||||
// staff member.
|
||||
if (IGNORE_FLAGS_AGAINST_STAFF) {
|
||||
if (action_type === 'FLAG') {
|
||||
|
||||
// If the item is a user, and this is a flag. Check to see if they are
|
||||
// staff, if they are, don't permit the flag.
|
||||
if (item_type === 'USERS' && item.isStaff()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let action = await ActionsService.create({
|
||||
// Create the action itself.
|
||||
let action = await Actions.create({
|
||||
item_id,
|
||||
item_type,
|
||||
user_id: user.id,
|
||||
@@ -31,17 +83,17 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
metadata
|
||||
});
|
||||
|
||||
if (item_type === 'USERS' && action_type === 'FLAG') {
|
||||
if (action_type === 'FLAG') {
|
||||
if (item_type === 'USERS') {
|
||||
|
||||
// Set the user as pending if it was a user flag and user has no Admin, Staff or Moderation roles
|
||||
let user = await UsersService.findById(item_id);
|
||||
if(!user.isStaff()){
|
||||
// Set the user's status as pending, as we need to review it.
|
||||
await UsersService.setStatus(item_id, 'PENDING');
|
||||
}
|
||||
}
|
||||
} else if (item_type === 'COMMENTS') {
|
||||
|
||||
if (comment) {
|
||||
pubsub.publish('commentFlagged', comment);
|
||||
// The item is a comment, and this is a flag. Push that the comment was
|
||||
// flagged, don't wait for it to finish.
|
||||
pubsub.publish('commentFlagged', item);
|
||||
}
|
||||
}
|
||||
|
||||
return action;
|
||||
@@ -53,16 +105,25 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
* @param {String} id the id of the action to delete
|
||||
* @return {Promise} resolves to the deleted action, or null if not found.
|
||||
*/
|
||||
const deleteAction = ({user}, {id}) => {
|
||||
return ActionsService.delete({id, user_id: user.id});
|
||||
const deleteAction = (ctx, {id}) => {
|
||||
const {
|
||||
user,
|
||||
connectors: {
|
||||
services: {
|
||||
Actions,
|
||||
},
|
||||
},
|
||||
} = ctx;
|
||||
|
||||
return Actions.delete({id, user_id: user.id});
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
if (context.user && context.user.can(CREATE_ACTION, DELETE_ACTION)) {
|
||||
module.exports = (ctx) => {
|
||||
if (ctx.user && ctx.user.can(CREATE_ACTION, DELETE_ACTION)) {
|
||||
return {
|
||||
Action: {
|
||||
create: (action) => createAction(context, action),
|
||||
delete: (action) => deleteAction(context, action)
|
||||
create: (action) => createAction(ctx, action),
|
||||
delete: (action) => deleteAction(ctx, action)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ const {
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const {
|
||||
DISABLE_AUTOFLAG_SUSPECT_WORDS
|
||||
DISABLE_AUTOFLAG_SUSPECT_WORDS,
|
||||
IGNORE_FLAGS_AGAINST_STAFF,
|
||||
} = require('../../config');
|
||||
|
||||
const debug = require('debug')('talk:graph:mutators:tags');
|
||||
@@ -292,7 +293,7 @@ const moderationPhases = [
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the comment's length exeeds maximum.
|
||||
// This phase checks to see if the comment's length exceeds maximum.
|
||||
(context, comment, {assetSettings: {charCountEnable, charCount}}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
@@ -313,6 +314,15 @@ const moderationPhases = [
|
||||
}
|
||||
},
|
||||
|
||||
// If a given user is a staff member, always approve their comment.
|
||||
(context) => {
|
||||
if (IGNORE_FLAGS_AGAINST_STAFF && context.user && context.user.isStaff()) {
|
||||
return {
|
||||
status: 'ACCEPTED',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks the comment if it has any links in it if the check is
|
||||
// enabled.
|
||||
(context, comment, {assetSettings: {premodLinksEnable}}) => {
|
||||
@@ -362,7 +372,7 @@ const moderationPhases = [
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the comment was already perscribed a status.
|
||||
// This phase checks to see if the comment was already prescribed a status.
|
||||
(context, comment) => {
|
||||
|
||||
// If the status was already defined, don't redefine it. It's only defined
|
||||
|
||||
@@ -157,10 +157,22 @@ function getReactionConfig(reaction) {
|
||||
RootMutation: {
|
||||
[`create${Reaction}Action`]: async (_, {input: {item_id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
|
||||
const comment = await Comments.get.load(item_id);
|
||||
if (!comment) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
let action;
|
||||
try {
|
||||
action = await Action.create({item_id, item_type: 'COMMENTS', action_type: REACTION});
|
||||
const action = await Action.create({item_id, item_type: 'COMMENTS', action_type: REACTION});
|
||||
|
||||
if (pubsub) {
|
||||
|
||||
// The comment is needed to allow better filtering e.g. by asset_id.
|
||||
pubsub.publish(`${reaction}ActionCreated`, {action, comment});
|
||||
}
|
||||
|
||||
return {
|
||||
[reaction]: action,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof errors.ErrAlreadyExists) {
|
||||
return err.metadata.existing;
|
||||
@@ -168,16 +180,6 @@ function getReactionConfig(reaction) {
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (pubsub) {
|
||||
|
||||
// The comment is needed to allow better filtering e.g. by asset_id.
|
||||
pubsub.publish(`${reaction}ActionCreated`, {action, comment});
|
||||
}
|
||||
|
||||
return {
|
||||
[reaction]: action,
|
||||
};
|
||||
},
|
||||
[`delete${Reaction}Action`]: async (_, {input: {id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
|
||||
const action = await Action.delete({id});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
background-color: #616161;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-smart-import')({ /* ...options */ }),
|
||||
require('precss')({ /* ...options */ }),
|
||||
require('autoprefixer')({ /* ...options */ })
|
||||
]
|
||||
require('postcss-smart-import'),
|
||||
require('precss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
||||
|
||||
+28
-12
@@ -395,18 +395,34 @@ module.exports = class UsersService {
|
||||
throw new Error(`status ${status} is not supported`);
|
||||
}
|
||||
|
||||
// TODO: current updating status behavior is weird.
|
||||
// once a user has been `APPROVED` its status cannot be
|
||||
// changed anymore.
|
||||
const user = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
status: {
|
||||
$ne: 'APPROVED'
|
||||
}
|
||||
}, {
|
||||
// Compose the query.
|
||||
const query = {id};
|
||||
|
||||
// Insert extra validations into the query.
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
case 'BANNED':
|
||||
case 'APPROVED':
|
||||
|
||||
// A user cannot become change their status from what it is already.
|
||||
query.status = {
|
||||
$ne: status,
|
||||
};
|
||||
break;
|
||||
case 'PENDING':
|
||||
|
||||
// A user cannot become pending if they are already approved, pending, or
|
||||
// banned
|
||||
query.status = {
|
||||
$nin: [status, 'APPROVED', 'BANNED'],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
const user = await UserModel.findOneAndUpdate(query, {
|
||||
$set: {
|
||||
status
|
||||
}
|
||||
status,
|
||||
},
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
@@ -426,8 +442,8 @@ module.exports = class UsersService {
|
||||
};
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user