diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css index 7036998d1..fc2ba9931 100644 --- a/client/coral-admin/src/components/ModerationList.css +++ b/client/coral-admin/src/components/ModerationList.css @@ -186,4 +186,5 @@ .actionButton { transform: scale(.8); margin: 0; + width: 140px; } diff --git a/client/coral-admin/src/containers/Dashboard/ActivityWidget.js b/client/coral-admin/src/containers/Dashboard/ActivityWidget.js new file mode 100644 index 000000000..a6e293b82 --- /dev/null +++ b/client/coral-admin/src/containers/Dashboard/ActivityWidget.js @@ -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 ( +
+

Articles with the most conversations

+
+

{lang.t('streams.article')}

+

{lang.t('dashboard.comment_count')}

+
+
+ { + assets.length + ? assets.map(asset => { + return ( +
+ Moderate +

{asset.commentCount}

+ +

{asset.title}

+ +

{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

+
+ ); + }) + :
{lang.t('dashboard.no_activity')}
+ } +
+
+ ); +}; + +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; diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index 5b4a20282..602a48d7f 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -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 ; } - 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 (

this.setState({noteHidden: true})}> + onClick={this.dismissNote}> × {lang.t('dashboard.next-update', this.formatTime())} {lang.t('dashboard.auto-update')}

- +
); diff --git a/client/coral-admin/src/containers/Dashboard/FlagWidget.js b/client/coral-admin/src/containers/Dashboard/FlagWidget.js index 7d2ffe17a..468875d3d 100644 --- a/client/coral-admin/src/containers/Dashboard/FlagWidget.js +++ b/client/coral-admin/src/containers/Dashboard/FlagWidget.js @@ -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 (
@@ -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; diff --git a/client/coral-admin/src/containers/Dashboard/LikeWidget.js b/client/coral-admin/src/containers/Dashboard/LikeWidget.js index 436fbcaf3..b878c281b 100644 --- a/client/coral-admin/src/containers/Dashboard/LikeWidget.js +++ b/client/coral-admin/src/containers/Dashboard/LikeWidget.js @@ -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 (
@@ -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; diff --git a/client/coral-admin/src/containers/Dashboard/Widget.css b/client/coral-admin/src/containers/Dashboard/Widget.css index 401cd1fe7..587c51ada 100644 --- a/client/coral-admin/src/containers/Dashboard/Widget.css +++ b/client/coral-admin/src/containers/Dashboard/Widget.css @@ -19,7 +19,6 @@ padding-left: 10px; font-size: 1.5rem; font-weight: bold; - background-color: #e0e0e0; } .widgetTable { diff --git a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql index 37335aeaa..c77fbc32b 100644 --- a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql +++ b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql @@ -4,6 +4,7 @@ fragment metrics on Asset { url author created_at + commentCount action_summaries { type: __typename actionCount diff --git a/client/coral-admin/src/graphql/queries/metricsQuery.graphql b/client/coral-admin/src/graphql/queries/metricsQuery.graphql index 42a9fb70e..f0dff8965 100644 --- a/client/coral-admin/src/graphql/queries/metricsQuery.graphql +++ b/client/coral-admin/src/graphql/queries/metricsQuery.graphql @@ -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 + } } diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index ff111ea1b..8a8c0ee14 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -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": { diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 0a3cdb78b..20060f474 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -157,31 +157,31 @@ class Comment extends React.Component {
- - - - - setActiveReplyBox(comment.id)} - parentCommentId={parentId || comment.id} - currentUserId={currentUser && currentUser.id} - banned={false} /> - - - - - - -
+ + + + + setActiveReplyBox(comment.id)} + parentCommentId={parentId || comment.id} + currentUserId={currentUser && currentUser.id} + banned={false} /> + + + + + + +
diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index d115aedca..d95f5c9ec 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -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", diff --git a/client/coral-plugin-flags/translations.json b/client/coral-plugin-flags/translations.json index caac6cbf1..255e13f3c 100644 --- a/client/coral-plugin-flags/translations.json +++ b/client/coral-plugin-flags/translations.json @@ -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": { diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index a6fe5afe4..a0408ed4d 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -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}), } } }); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index bdcdcef78..e2836c51d 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -63,6 +63,10 @@ const RootQuery = { return null; } + if (sort === 'ACTIVITY') { + return Assets.getActivity({from, to, limit}); + } + return Assets.get({from, to, sort, limit}); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 3000ea091..e656d0183 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -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.