mirror of
https://github.com/wassname/talk.git
synced 2026-07-17 11:33:39 +08:00
Merge branch 'config-embed' of github.com:coralproject/talk into config-embed
This commit is contained in:
@@ -20,5 +20,6 @@ plugins/*
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -4,7 +4,7 @@ export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote});
|
||||
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
|
||||
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
|
||||
|
||||
// hide shortcuts note
|
||||
|
||||
@@ -8,14 +8,14 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => {
|
||||
const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => {
|
||||
e.preventDefault();
|
||||
handleBanUser({userId})
|
||||
.then(handleClose)
|
||||
.then(() => rejectComment({commentId}));
|
||||
.then(() => commentStatus === 'REJECTED' ? null : rejectComment({commentId}));
|
||||
};
|
||||
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, showRejectedNote}) => (
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, commentStatus, showRejectedNote}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banuserDialog"
|
||||
@@ -36,7 +36,7 @@ const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, c
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
|
||||
{lang.t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, handleBanUser, rejectComment, handleClose)} raised>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, commentStatus, handleBanUser, rejectComment, handleClose)} raised>
|
||||
{lang.t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -193,10 +193,12 @@
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reject__active, .rejected__active {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {getMetrics} from 'coral-admin/src/graphql/queries';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import ActivityWidget from './ActivityWidget';
|
||||
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
|
||||
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
@@ -43,12 +42,7 @@ const mapStateToProps = state => {
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(mapStateToProps),
|
||||
getMetrics
|
||||
)(Dashboard);
|
||||
|
||||
@@ -43,12 +43,13 @@ class ModerationContainer extends Component {
|
||||
const {acceptComment, rejectComment} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
const comments = this.getComments();
|
||||
const commentId = {commentId: comments[selectedIndex].id};
|
||||
const comment = comments[selectedIndex];
|
||||
const commentId = {commentId: comment.id};
|
||||
|
||||
if (accept) {
|
||||
acceptComment(commentId);
|
||||
comment.status !== 'ACCEPTED' && acceptComment(commentId);
|
||||
} else {
|
||||
rejectComment(commentId);
|
||||
comment.status !== 'REJECTED' && rejectComment(commentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +186,7 @@ class ModerationContainer extends Component {
|
||||
open={moderation.banDialog}
|
||||
user={moderation.user}
|
||||
commentId={moderation.commentId}
|
||||
commentStatus={moderation.commentStatus}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
handleBanUser={props.banUser}
|
||||
showRejectedNote={moderation.showRejectedNote}
|
||||
@@ -212,7 +214,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
singleView: () => dispatch(singleView()),
|
||||
updateAssets: assets => dispatch(updateAssets(assets)),
|
||||
fetchSettings: () => dispatch(fetchSettings()),
|
||||
showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)),
|
||||
showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
hideShortcutsNote: () => dispatch(hideShortcutsNote()),
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import timeago from 'timeago.js';
|
||||
import Linkify from 'react-linkify';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {Link} from 'react-router';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import styles from './styles.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import styles from './styles.css';
|
||||
import CommentType from './CommentType';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
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();
|
||||
|
||||
@@ -18,11 +19,19 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) => {
|
||||
const Comment = ({
|
||||
actions = [],
|
||||
comment,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
...props
|
||||
}) => {
|
||||
const links = linkify.getMatches(comment.body);
|
||||
const linkText = links ? links.map(link => link.raw) : [];
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
|
||||
const flagActions =
|
||||
comment.actions &&
|
||||
comment.actions.filter(a => a.__typename === 'FlagAction');
|
||||
let commentType = '';
|
||||
if (comment.status === 'PREMOD') {
|
||||
commentType = 'premod';
|
||||
@@ -33,12 +42,17 @@ const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) =
|
||||
// since words are checked against word boundaries on the backend,
|
||||
// this should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support this out of the box.
|
||||
const searchWords = [...suspectWords, ...bannedWords].filter(w => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
|
||||
}).concat(linkText);
|
||||
const searchWords = [...suspectWords, ...bannedWords]
|
||||
.filter(w => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
|
||||
})
|
||||
.concat(linkText);
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
|
||||
<li
|
||||
tabIndex={props.index}
|
||||
className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
@@ -46,53 +60,95 @@ const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) =
|
||||
{comment.user.name}
|
||||
</span>
|
||||
<span className={styles.created}>
|
||||
{timeago().format(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={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
|
||||
<BanUserButton
|
||||
user={comment.user}
|
||||
onClick={() =>
|
||||
props.showBanUserDialog(
|
||||
comment.user,
|
||||
comment.id,
|
||||
comment.status,
|
||||
comment.status !== 'REJECTED'
|
||||
)}
|
||||
/>
|
||||
<CommentType type={commentType} />
|
||||
</div>
|
||||
{comment.user.status === 'banned' ?
|
||||
<span className={styles.banned}>
|
||||
<Icon name='error_outline'/>
|
||||
{lang.t('comment.banned_user')}
|
||||
</span>
|
||||
{comment.user.status === 'banned'
|
||||
? <span className={styles.banned}>
|
||||
<Icon name="error_outline" />
|
||||
{lang.t('comment.banned_user')}
|
||||
</span>
|
||||
: null}
|
||||
<Slot fill="adminCommentInfoBar" comment={comment} />
|
||||
</div>
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset && (
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate →</Link>
|
||||
)}
|
||||
{!props.currentAsset &&
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate →</Link>}
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<Highlighter
|
||||
searchWords={searchWords}
|
||||
textToHighlight={comment.body} /> <a className={styles.external} href={`${comment.asset.url}#${comment.id}`} target="_blank"><Icon name='open_in_new' /> {lang.t('comment.view_context')}</a>
|
||||
textToHighlight={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}#${comment.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="open_in_new" /> {lang.t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
<Slot fill="adminCommentContent" comment={comment} />
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
{links
|
||||
? <span className={styles.hasLinks}>
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
: null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) => {
|
||||
const active = (action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return <ActionButton key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() => props.acceptComment({commentId: comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: comment.id})} />;
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: props.acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: props.rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Slot fill="adminSideActions" comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
flagActions && flagActions.length
|
||||
? <FlagBox actions={flagActions} actionSummaries={flagActionSummaries} />
|
||||
: null
|
||||
}
|
||||
<div>
|
||||
<Slot fill="adminCommentDetailArea" comment={comment} />
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const initialState = Map({
|
||||
modalOpen: false,
|
||||
user: Map({}),
|
||||
commentId: null,
|
||||
commentStatus: null,
|
||||
banDialog: false,
|
||||
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show'
|
||||
});
|
||||
@@ -14,12 +15,14 @@ export default function moderation (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.HIDE_BANUSER_DIALOG:
|
||||
return state
|
||||
.set('banDialog', false);
|
||||
.set('banDialog', false)
|
||||
.set('commentStatus', null);
|
||||
case actions.SHOW_BANUSER_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
showRejectedNote: action.showRejectedNote,
|
||||
banDialog: true
|
||||
});
|
||||
|
||||
@@ -95,18 +95,12 @@ Slots properties take an`Array` so we can add as many components as we want.
|
||||
|
||||
`Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.`
|
||||
|
||||
In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order componenets that expose a simple api.
|
||||
In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order components that expose a simple api.
|
||||
|
||||
### Reactions
|
||||
|
||||
Reactions provide users the ability to 'like', 'respect', etc... comments.
|
||||
|
||||
```
|
||||
<Reaction name="love">
|
||||
<img class="coralsrc="" />
|
||||
</Reaction>
|
||||
```
|
||||
|
||||
Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples.
|
||||
|
||||
### Comment Stream
|
||||
@@ -115,11 +109,7 @@ Comment streams may be created with filtering and ordering in place:
|
||||
|
||||
* filter by user
|
||||
* filter by tag
|
||||
* sort by date ascending / descrnding
|
||||
|
||||
```
|
||||
<CommentStream userFilter="[userId]" tagFilter="[tag]" sort="[ascending|descending]"></CommentStream>
|
||||
```
|
||||
* sort by date ascending / descending
|
||||
|
||||
### Comment Commit hooks
|
||||
|
||||
|
||||
@@ -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,8 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css'
|
||||
|
||||
export default (props) => (
|
||||
<div className={styles.box}>
|
||||
Comment Status: {props.comment.status}
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import Box from './Box';
|
||||
import {Button} from 'coral-ui'
|
||||
import styles from './styles.css';
|
||||
|
||||
export default class Footer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
show: false
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.setState(state => ({
|
||||
show: !state.show
|
||||
}))
|
||||
}
|
||||
|
||||
render() {
|
||||
const {show} = this.state;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button cStyle="darkGrey" onClick={this.handleClick}>
|
||||
Show Comment Status
|
||||
</Button>
|
||||
{show ? <Box comment={this.props.comment} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.container {
|
||||
padding: 0 14px 10px;
|
||||
}
|
||||
|
||||
.box {
|
||||
font-size: 12px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Container from './components/Container';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
adminCommentDetailArea: [Container],
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
const {readFileSync} = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {};
|
||||
Reference in New Issue
Block a user