mirror of
https://github.com/wassname/talk.git
synced 2026-07-05 03:21:53 +08:00
Merge branch 'master' into recaptcha-support
This commit is contained in:
@@ -186,4 +186,5 @@
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const ActivityWidget = ({assets}) => {
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Articles with the most conversations</h2>
|
||||
<div className={styles.widgetHead}>
|
||||
<p>{lang.t('streams.article')}</p>
|
||||
<p>{lang.t('dashboard.comment_count')}</p>
|
||||
</div>
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map(asset => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{asset.commentCount}</p>
|
||||
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</Link>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{lang.t('dashboard.no_activity')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActivityWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
commentCount: PropTypes.number,
|
||||
author: PropTypes.string,
|
||||
created_at: PropTypes.string
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default ActivityWidget;
|
||||
@@ -4,7 +4,7 @@ import {compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {getMetrics} from 'coral-admin/src/graphql/queries';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import LikeWidget from './LikeWidget';
|
||||
import ActivityWidget from './ActivityWidget';
|
||||
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
@@ -15,9 +15,22 @@ const refreshIntervalSeconds = 60 * 5;
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
|
||||
state = {
|
||||
noteHidden: false,
|
||||
secondsUntilRefresh: refreshIntervalSeconds
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
try {
|
||||
if (window.localStorage.getItem('coral:dashboardNote') === null) {
|
||||
window.localStorage.setItem('coral:dashboardNote', 'show');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Private Mode in some browsers.
|
||||
}
|
||||
|
||||
this.state = {
|
||||
secondsUntilRefresh: refreshIntervalSeconds,
|
||||
dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show'
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
@@ -31,6 +44,16 @@ class Dashboard extends React.Component {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
dismissNote = () => {
|
||||
try {
|
||||
window.localStorage.setItem('coral:dashboardNote', 'hide');
|
||||
} catch (e) {
|
||||
|
||||
// when setItem fails in Safari Private mode
|
||||
this.setState({dashboardNote: 'hide'});
|
||||
}
|
||||
}
|
||||
|
||||
formatTime = () => {
|
||||
const minutes = Math.floor(this.state.secondsUntilRefresh / 60);
|
||||
let seconds = (this.state.secondsUntilRefresh % 60).toString();
|
||||
@@ -47,20 +70,22 @@ class Dashboard extends React.Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const {data: {assetsByLike, assetsByFlag}} = this.props;
|
||||
const {data: {assetsByActivity, assetsByFlag}} = this.props;
|
||||
const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' ||
|
||||
this.state.dashboardNote === 'hide'; // for Safari Incognito
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p
|
||||
style={{display: this.state.noteHidden ? 'none' : 'block'}}
|
||||
style={{display: hideReloadNote ? 'none' : 'block'}}
|
||||
className={styles.autoUpdate}
|
||||
onClick={() => this.setState({noteHidden: true})}>
|
||||
onClick={this.dismissNote}>
|
||||
<b>×</b>
|
||||
<Icon name='timer' /> <strong>{lang.t('dashboard.next-update', this.formatTime())}</strong> {lang.t('dashboard.auto-update')}
|
||||
</p>
|
||||
<div className={styles.Dashboard}>
|
||||
<FlagWidget assets={assetsByFlag} />
|
||||
<LikeWidget assets={assetsByLike} />
|
||||
<ActivityWidget assets={assetsByActivity} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -6,8 +6,7 @@ import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const FlagWidget = (props) => {
|
||||
const {assets} = props;
|
||||
const FlagWidget = ({assets}) => {
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
@@ -39,4 +38,14 @@ const FlagWidget = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
FlagWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
action_summaries: PropTypes.array,
|
||||
author: PropTypes.string,
|
||||
created_at: PropTypes.string
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default FlagWidget;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -6,9 +6,7 @@ import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const LikeWidget = (props) => {
|
||||
|
||||
const {assets} = props;
|
||||
const LikeWidget = ({assets}) => {
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
@@ -40,4 +38,14 @@ const LikeWidget = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
LikeWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
action_summaries: PropTypes.array,
|
||||
author: PropTypes.string,
|
||||
created_at: PropTypes.string
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default LikeWidget;
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
padding-left: 10px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.widgetTable {
|
||||
|
||||
@@ -4,6 +4,7 @@ fragment metrics on Asset {
|
||||
url
|
||||
author
|
||||
created_at
|
||||
commentCount
|
||||
action_summaries {
|
||||
type: __typename
|
||||
actionCount
|
||||
|
||||
@@ -7,4 +7,7 @@ query Metrics ($from: Date!, $to: Date!) {
|
||||
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
|
||||
...metrics
|
||||
}
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
|
||||
...metrics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
|
||||
"no_likes": "There have been no likes in the last 5 minutes. All quiet.",
|
||||
"flags": "Flags",
|
||||
"no_activity": "There haven't been any comments anywhere in the last five minutes.",
|
||||
"comment_count": "comments"
|
||||
},
|
||||
"streams": {
|
||||
@@ -244,6 +245,7 @@
|
||||
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
|
||||
"no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
|
||||
"flags": "Marcados",
|
||||
"no_activity": "No hubo comentarios en los ultimos 5 minutos",
|
||||
"comment_count": "comentarios"
|
||||
},
|
||||
"streams": {
|
||||
|
||||
@@ -157,31 +157,31 @@ class Comment extends React.Component {
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<ActionButton>
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<IfUserCanModifyBest user={currentUser}>
|
||||
<BestButton
|
||||
isBest={commentIsBest(comment)}
|
||||
addBest={addBestTag}
|
||||
removeBest={removeBestTag} />
|
||||
</IfUserCanModifyBest>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<ActionButton>
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<IfUserCanModifyBest user={currentUser}>
|
||||
<BestButton
|
||||
isBest={commentIsBest(comment)}
|
||||
addBest={addBestTag}
|
||||
removeBest={removeBestTag} />
|
||||
</IfUserCanModifyBest>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<ActionButton>
|
||||
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"successNameUpdate": "Your username has been updated",
|
||||
"contentNotAvailable": "This content is not available",
|
||||
"loadMore": "View more",
|
||||
"bannedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
|
||||
"bannedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Report, or write comments. Please contact us if you have any questions.",
|
||||
"editName": {
|
||||
"msg": "Your account is currently suspended because your username has been deemed inappropriate. To restore your account, please enter a new username. You may contact moderator@fakeurl.com for more information.",
|
||||
"msg": "Your account is currently suspended because your username has been deemed inappropriate. To restore your account, please enter a new username. Please contact us if you have any questions.",
|
||||
"label": "New Username",
|
||||
"button": "Submit",
|
||||
"error": "Usernames can contain letters, numbers and _ only"
|
||||
@@ -41,7 +41,7 @@
|
||||
"successUpdateSettings": "La configuración de este articulo fue actualizada",
|
||||
"successBioUpdate": "Tu bio fue actualizada",
|
||||
"contentNotAvailable": "El contenido no se encuentra disponible",
|
||||
"bannedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
|
||||
"bannedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios.",
|
||||
"editNameMsg": "",
|
||||
"loadMore": "Ver más",
|
||||
"newCount": "Ver {0} {1} más",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"step-1-header": "Report an issue",
|
||||
"step-2-header": "Help us understand",
|
||||
"step-3-header": "Thank you for your input",
|
||||
"flag-username": "Flag username",
|
||||
"flag-comment": "Flag comment",
|
||||
"flag-username": "Report username",
|
||||
"flag-comment": "Report comment",
|
||||
"continue": "Continue",
|
||||
"done": "Done",
|
||||
"no-agree-comment": "I don't agree with this comment",
|
||||
@@ -20,8 +20,8 @@
|
||||
"no-like-bio": "I don't like this bio",
|
||||
"marketing": "This looks like an ad/marketing",
|
||||
"user-impersonating": "This user is impersonating",
|
||||
"thank-you": "We value your safety and feedback. A moderator will review your flag.",
|
||||
"flag-reason": "Reason for flag (Optional)",
|
||||
"thank-you": "We value your safety and feedback. A moderator will review your report.",
|
||||
"flag-reason": "Reason for reporting (Optional)",
|
||||
"other": "Other"
|
||||
},
|
||||
"es": {
|
||||
|
||||
@@ -3,6 +3,53 @@ const DataLoader = require('dataloader');
|
||||
const {objectCacheKeyFn} = require('./util');
|
||||
|
||||
const ActionModel = require('../../models/action');
|
||||
const CommentModel = require('../../models/comment');
|
||||
|
||||
/**
|
||||
* Returns the assets which have had comments made within the last time period.
|
||||
*/
|
||||
const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => {
|
||||
let assetMetrics = [];
|
||||
|
||||
return CommentModel.aggregate([
|
||||
{$match: {
|
||||
parent_id: null,
|
||||
created_at: {
|
||||
$gt: from,
|
||||
$lt: to
|
||||
}
|
||||
}},
|
||||
{$group: {
|
||||
_id: '$asset_id',
|
||||
commentCount: {
|
||||
$sum: 1
|
||||
}
|
||||
}},
|
||||
{$project: {
|
||||
_id: false,
|
||||
asset_id: '$_id',
|
||||
commentCount: '$commentCount'
|
||||
}},
|
||||
{$sort: {
|
||||
commentCount: -1
|
||||
}},
|
||||
{$limit: limit}
|
||||
])
|
||||
.then((results) => {
|
||||
assetMetrics = results;
|
||||
|
||||
return Assets.getByID.loadMany(results.map((result) => result.asset_id));
|
||||
})
|
||||
.then((assets) => assets.map((asset, i) => {
|
||||
|
||||
// We're leveraging the fact that the comments returned by the aggregation
|
||||
// query are in the request order that we just made, it's what the
|
||||
// Assets.getByID loader does.
|
||||
asset.commentCount = assetMetrics[i].commentCount;
|
||||
|
||||
return asset;
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of assets with action metadata included on the models.
|
||||
@@ -208,10 +255,11 @@ module.exports = (context) => ({
|
||||
cacheKeyFn: objectCacheKeyFn('from', 'to')
|
||||
}),
|
||||
Assets: {
|
||||
get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit})
|
||||
get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit}),
|
||||
getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}),
|
||||
},
|
||||
Comments: {
|
||||
get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit})
|
||||
get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit}),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,6 +63,10 @@ const RootQuery = {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sort === 'ACTIVITY') {
|
||||
return Assets.getActivity({from, to, limit});
|
||||
}
|
||||
|
||||
return Assets.get({from, to, sort, limit});
|
||||
},
|
||||
|
||||
|
||||
+17
-1
@@ -479,6 +479,22 @@ enum USER_STATUS {
|
||||
APPROVED
|
||||
}
|
||||
|
||||
# Metrics for the assets.
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a LikeAction.
|
||||
LIKE
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
|
||||
# Represents a don't agree action.
|
||||
DONTAGREE
|
||||
|
||||
# Represents activity.
|
||||
ACTIVITY
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
|
||||
# Site wide settings and defaults.
|
||||
@@ -506,7 +522,7 @@ type RootQuery {
|
||||
|
||||
# Asset metrics related to user actions are saturated into the assets
|
||||
# returned.
|
||||
assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!]
|
||||
assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
|
||||
|
||||
# Comment metrics related to user actions are saturated into the comments
|
||||
# returned.
|
||||
|
||||
Reference in New Issue
Block a user