-
- {lang.t('modqueue.premod')}
-
-
- {lang.t('modqueue.rejected')}
-
-
- {lang.t('modqueue.flagged')}
-
+class ModerationMenu extends Component {
+ state = {
+ sort: 'REVERSE_CHRONOLOGICAL',
+ }
+
+ static propTypes = {
+ premodCount: PropTypes.number.isRequired,
+ rejectedCount: PropTypes.number.isRequired,
+ flaggedCount: PropTypes.number.isRequired,
+ asset: PropTypes.shape({
+ id: PropTypes.string
+ })
+ }
+
+ selectSort = (sort) => {
+ this.setState({sort});
+ this.props.modQueueResort(sort);
+ }
+
+ render() {
+ const {asset, premodCount, rejectedCount, flaggedCount} = this.props;
+ const premodPath = asset ? `/admin/moderate/premod/${asset.id}` : '/admin/moderate/premod';
+ const rejectPath = asset ? `/admin/moderate/rejected/${asset.id}` : '/admin/moderate/rejected';
+ const flagPath = asset ? `/admin/moderate/flagged/${asset.id}` : '/admin/moderate/flagged';
+ return (
+
+
+
+
+
+ {lang.t('modqueue.premod')}
+
+
+ {lang.t('modqueue.rejected')}
+
+
+ {lang.t('modqueue.flagged')}
+
+
+
this.selectSort(sort)}>
+
+
+
-
- );
-};
-
-ModerationMenu.propTypes = {
- premodCount: PropTypes.number.isRequired,
- rejectedCount: PropTypes.number.isRequired,
- flaggedCount: PropTypes.number.isRequired,
- asset: PropTypes.shape({
- id: PropTypes.string
- })
-};
+ );
+ }
+}
export default ModerationMenu;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/styles.css b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
index e2fd31911..59f47faad 100644
--- a/client/coral-admin/src/containers/ModerationQueue/components/styles.css
+++ b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
@@ -8,6 +8,12 @@
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
+ display: flex;
+ justify-content: space-between;
+}
+
+.tabBarPadding {
+ width: 150px;
}
.tab {
@@ -130,7 +136,7 @@ span {
display: none;
}
- &.singleView .listItem.activeItem {
+ &.singleView .listItem.selected {
display: block;
height: 100%;
font-size: 1.5em;
@@ -141,7 +147,6 @@ span {
position: fixed;
bottom: 60px;
left: 25%;
- margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
@@ -156,16 +161,20 @@ span {
.listItem {
border-bottom: 1px solid #e0e0e0;
- font-size: 16px;
+ font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
- padding: 16px 14px;
position: relative;
- transition: box-shadow 200ms;
- margin-top: 0;
+ transition: all 200ms;
+ padding: 10px 0 0;
+ min-height: 220px;
+ .container {
+ padding: 0 14px;
+ min-height: 180px;
+ }
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
@@ -175,6 +184,11 @@ span {
border-bottom: none;
}
+ &.selected {
+ max-width: 670px;
+ max-height: 410px;
+ }
+
.context {
a {
color: #f36451;
@@ -184,11 +198,8 @@ span {
}
.sideActions {
- position: absolute;
- right: 0;
height: 100%;
top: 0;
- padding: 40px 18px;
box-sizing: border-box;
}
@@ -198,6 +209,7 @@ span {
justify-content: space-between;
.author {
+ font-weight: 600;
min-width: 230px;
display: flex;
align-items: center;
@@ -207,6 +219,9 @@ span {
.itemBody {
display: flex;
justify-content: space-between;
+ font-size: 14px;
+ line-height: 1.5;
+ font-weight: 300;
}
.avatar {
@@ -222,7 +237,8 @@ span {
.created {
color: #666;
font-size: 13px;
- margin-left: 40px;
+ margin-left: 15px;
+ line-height: 1px;
}
.actionButton {
@@ -233,10 +249,10 @@ span {
.body {
margin-top: 0px;
flex: 1;
- font-size: 0.88em;
color: black;
max-width: 500px;
word-wrap: break-word;
+ font-weight: 300;
}
.flagged {
@@ -304,20 +320,29 @@ span {
}
.Comment {
+
.moderateArticle {
- font-size: 12px;
+ font-size: 14px;
+ margin: 10px 0;
+ font-weight: 500;
+ line-height: 1.2;
+ max-width: 500px;
+
a {
display: inline-block;
- color: #679af3;
+ color: #063b9a;
text-decoration: none;
- font-size: 1em;
- font-weight: 400;
+ font-weight: 500;
letter-spacing: .5px;
- font-size: 12px;
margin-left: 10px;
+ font-size: 13px;
+ margin-left: 5px;
+ padding-bottom: 0px;
+ border-bottom: solid 1px;
+ line-height: 16px;
+
&:hover {
- text-decoration: underline;
opacity: .9;
cursor: pointer;
}
@@ -325,12 +350,40 @@ span {
}
}
-.flagBox {
- max-width: 480px;
- border-top: 1px solid rgba(66, 66, 66, 0.12);
- h3 {
- font-size: 14px;
- margin: 0;
- font-weight: 500;
+.selectField {
+ position: relative;
+ width: 140px;
+ height: 36px;
+ top: 5px;
+ margin-right: 10px;
+ background: #FFF;
+ padding: 10px 15px;
+ box-sizing: border-box;
+ border-radius: 2px;
+ box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
+
+ > div {
+ padding: 0;
}
+
+ i {
+ position: absolute;
+ top: 7px;
+ right: 7px;
+ }
+
+ input {
+ padding: 0;
+ font-size: 13px;
+ letter-spacing: 0.7px;
+ font-weight: 400;
+ }
+
+ label {
+ top: -4px;
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
}
diff --git a/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js b/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js
index d0ac64070..09066831f 100644
--- a/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js
+++ b/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js
@@ -1,13 +1,14 @@
export const actionsMap = {
- PREMOD: ['REJECT', 'APPROVE', 'BAN'],
- FLAGGED: ['REJECT', 'APPROVE', 'BAN'],
- REJECTED: ['APPROVE']
+ PREMOD: ['APPROVE', 'REJECT'],
+ FLAGGED: ['APPROVE', 'REJECT'],
+ REJECTED: ['APPROVE', 'REJECTED']
};
export const menuActionsMap = {
- 'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'},
- 'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'},
- 'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
- 'BAN': {status: 'BANNED', icon: 'not interested'},
+ 'REJECT': {status: 'REJECTED', text: 'Reject', icon: 'close', key: 'r'},
+ 'REJECTED': {status: 'REJECTED', text: 'Rejected', icon: 'close'},
+ 'APPROVE': {status: 'ACCEPTED', text: 'Approve', icon: 'done', key: 't'},
+ 'FLAGGED': {status: 'FLAGGED', text: 'Flag', icon: 'flag', filter: 'Untouched'},
+ 'BAN': {status: 'BANNED', text: 'Ban User', icon: 'not interested'},
'': {icon: 'done'}
};
diff --git a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
new file mode 100644
index 000000000..37335aeaa
--- /dev/null
+++ b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
@@ -0,0 +1,12 @@
+fragment metrics on Asset {
+ id
+ title
+ url
+ author
+ created_at
+ action_summaries {
+ type: __typename
+ actionCount
+ actionableItemCount
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index 3cc66c21a..e911402b0 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -1,33 +1,45 @@
import {graphql} from 'react-apollo';
-import MOST_FLAGS from './mostFlags.graphql';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
-
-export const mostFlags = graphql(MOST_FLAGS, {
- options: () => {
-
- // currently hard-coded per Greg's advice
- const fiveMinutesAgo = new Date();
- fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305);
- return {
- variables: {
- sort: 'FLAG',
- from: fiveMinutesAgo.toISOString(),
- to: new Date().toISOString()
- }
- };
- }
-});
+import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = null}}) => {
return {
variables: {
- asset_id: id
+ asset_id: id,
+ sort: 'REVERSE_CHRONOLOGICAL'
+ }
+ };
+ },
+ props: ({ownProps: {params: {id = null}}, data}) => ({
+ data,
+ modQueueResort: modQueueResort(id, data.fetchMore)
+ })
+});
+
+export const getMetrics = graphql(METRICS, {
+ options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
+
+ return {
+ variables: {
+ from: dashboardWindowStart,
+ to: dashboardWindowEnd
}
};
}
});
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY);
+
+export const modQueueResort = (id, fetchMore) => (sort) => {
+ return fetchMore({
+ query: MOD_QUEUE_QUERY,
+ variables: {
+ asset_id: id,
+ sort
+ },
+ updateQuery: (oldData, {fetchMoreResult:{data}}) => data
+ });
+};
diff --git a/client/coral-admin/src/graphql/queries/metricsQuery.graphql b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
new file mode 100644
index 000000000..42a9fb70e
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/assetMetricsView.graphql"
+
+query Metrics ($from: Date!, $to: Date!) {
+ assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
+ ...metrics
+ }
+ assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
+ ...metrics
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql
index 86cd26bbd..b4f2a20a0 100644
--- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql
+++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql
@@ -1,16 +1,18 @@
#import "../fragments/commentView.graphql"
-query ModQueue ($asset_id: ID) {
+query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
premod: comments(query: {
statuses: [PREMOD],
- asset_id: $asset_id
+ asset_id: $asset_id,
+ sort: $sort
}) {
...commentView
}
flagged: comments(query: {
action_type: FLAG,
asset_id: $asset_id,
- statuses: [NONE, PREMOD]
+ statuses: [NONE, PREMOD],
+ sort: $sort
}) {
...commentView
action_summaries {
@@ -22,7 +24,8 @@ query ModQueue ($asset_id: ID) {
}
rejected: comments(query: {
statuses: [REJECTED],
- asset_id: $asset_id
+ asset_id: $asset_id,
+ sort: $sort
}) {
...commentView
}
diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql
deleted file mode 100644
index 182ea0c8b..000000000
--- a/client/coral-admin/src/graphql/queries/mostFlags.graphql
+++ /dev/null
@@ -1,14 +0,0 @@
-query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) {
- metrics(from: $from, to: $to, sort: $sort) {
- id
- title
- url
- commentCount
- author
- created_at
- action_summaries {
- actionCount
- actionableItemCount
- }
- }
-}
diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js
index 095ef7aac..b52efdb85 100644
--- a/client/coral-admin/src/reducers/auth.js
+++ b/client/coral-admin/src/reducers/auth.js
@@ -4,7 +4,9 @@ import * as actions from '../constants/auth';
const initialState = Map({
loggedIn: false,
user: null,
- isAdmin: false
+ isAdmin: false,
+ loginError: null,
+ passwordRequestSuccess: null
});
export default function auth (state = initialState, action) {
@@ -25,6 +27,14 @@ export default function auth (state = initialState, action) {
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return initialState;
+ case actions.LOGIN_REQUEST:
+ return state.set('loginError', null);
+ case actions.LOGIN_FAILURE:
+ return state.set('loginError', action.message);
+ case actions.FETCH_FORGOT_PASSWORD_REQUEST:
+ return state.set('passwordRequestSuccess', null);
+ case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
+ return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
default :
return state;
}
diff --git a/client/coral-admin/src/reducers/install.js b/client/coral-admin/src/reducers/install.js
index 596fd16cd..1f0f079ff 100644
--- a/client/coral-admin/src/reducers/install.js
+++ b/client/coral-admin/src/reducers/install.js
@@ -1,4 +1,4 @@
-import {Map} from 'immutable';
+import {Map, List} from 'immutable';
import * as actions from '../constants/install';
@@ -6,7 +6,10 @@ const initialState = Map({
isLoading: false,
data: Map({
settings: Map({
- organizationName: ''
+ organizationName: '',
+ domains: Map({
+ whitelist: List()
+ })
}),
user: Map({
username: '',
@@ -33,6 +36,10 @@ const initialState = Map({
{
text: '2. Create your account',
step: 2
+ },
+ {
+ text: '3. Domain Whitelist',
+ step: 3
}],
installRequest: null,
installRequestError: null,
@@ -50,6 +57,9 @@ export default function install (state = initialState, action) {
case actions.GO_TO_STEP:
return state
.set('step', action.step);
+ case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
+ return state
+ .setIn(['data', 'settings', 'domains', 'whitelist'], action.value);
case actions.UPDATE_FORMDATA_SETTINGS:
return state
.setIn(['data', 'settings', action.name], action.value);
diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js
index a02cac198..67907b327 100644
--- a/client/coral-admin/src/reducers/settings.js
+++ b/client/coral-admin/src/reducers/settings.js
@@ -1,11 +1,22 @@
import {Map, List} from 'immutable';
import * as actions from '../actions/settings';
+// this is initialized here because
+// currently you have to reload the dashboard to get new stats
+// cleaner updates are planned in the future.
+// TODO: if there are more than two fields for the dashboard being created here,
+// please create a new reducer specifically for the Dashboard.
+const DASHBOARD_WINDOW_MINUTES = 5;
+let then = new Date();
+then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
+
const initialState = Map({
wordlist: Map({
banned: List(),
suspect: List()
}),
+ dashboardWindowStart: then.toISOString(),
+ dashboardWindowEnd: new Date().toISOString(),
domains: Map({
whitelist: List()
}),
@@ -22,7 +33,7 @@ export default function settings (state = initialState, action) {
.set('fetchSettingsError', null);
case actions.SETTINGS_RECEIVED:
return state.merge({
- fetchingSettings: null,
+ fetchingSettings: false,
fetchSettingsError: null,
...action.settings
});
@@ -32,7 +43,7 @@ export default function settings (state = initialState, action) {
.set('fetchSettingsError', action.error);
case actions.SETTINGS_UPDATED:
return state.merge({
- fetchingSettings: null,
+ fetchingSettings: false,
fetchSettingsError: null,
...action.settings
});
diff --git a/client/coral-admin/src/services/client.js b/client/coral-admin/src/services/client.js
index 40a539634..7f6a0567d 100644
--- a/client/coral-admin/src/services/client.js
+++ b/client/coral-admin/src/services/client.js
@@ -2,6 +2,7 @@ import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
export const client = new ApolloClient({
+ addTypename: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index bbc3b3419..188a26623 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -1,5 +1,8 @@
{
"en": {
+ "errors": {
+ "NOT_AUTHORIZED": "Your username or password is not recognized by our system."
+ },
"community": {
"username_and_email": "Username and Email",
"account_creation_date": "Account Creation Date",
@@ -63,6 +66,9 @@
"copy": "Copy to Clipboard"
},
"configure": {
+ "stream-settings": "Stream Settings",
+ "moderation-settings": "Moderation Settings",
+ "tech-settings": "Tech Settings",
"custom-css-url": "Custom CSS URL",
"custom-css-url-desc": "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.",
"dashboard": "Dashboard",
@@ -75,8 +81,6 @@
"include-text": "Include your text here.",
"comment-settings": "Settings",
"embed-comment-stream": "Embed Stream",
- "banned-word-header": "Write the banned words list",
- "suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"wordlist": "Banned Words",
@@ -98,8 +102,8 @@
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number.",
- "domain-list-title": "Domain Whitelist",
- "domain-list-text": "Some instructions on how to type the urls."
+ "domain-list-title": "Permitted Domains",
+ "domain-list-text": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
},
"bandialog": {
"ban_user": "Ban User?",
@@ -125,7 +129,9 @@
},
"dashboard": {
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
- "comment_count": "Comments"
+ "no_likes": "There have been no likes in the last 5 minutes. All quiet.",
+ "flags": "Flags",
+ "comment_count": "comments"
},
"streams": {
"empty_result": "No assets match this search. Maybe try widening your search?",
@@ -146,6 +152,9 @@
}
},
"es": {
+ "errors": {
+ "NOT_AUTHORIZED": "Acción no autorizada."
+ },
"community": {
"username_and_email": "Usuario y E-mail",
"account_creation_date": "Fecha de creación de la cuenta",
@@ -210,6 +219,9 @@
"username_flags": ""
},
"configure": {
+ "stream-settings": "Configuración de Comentarios",
+ "moderation-settings": "Configuración de Moderación",
+ "tech-settings": "Configuración Technical",
"custom-css-url": "URL CSS a medida",
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
"dashboard": "Panel",
@@ -223,8 +235,6 @@
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"wordlist": "Palabras Suspendidas y Suspechosas",
- "banned-word-header": "Escribir las palabras no permitidas",
- "suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"banned-words-title": "Banned words list",
@@ -247,21 +257,23 @@
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido.",
"domain-list-title": "Lista de Dominios Permitidos",
- "domain-list-text": "Instrucciones de como ingresar las URLs."
+ "domain-list-text": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com)."
},
"embedlink": {
"copy": "Copiar"
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
- "are_you_sure": "Estas segura que quieres suspender a {props.author.username}?",
+ "are_you_sure": "Estas segura que quieres suspender a {0}?",
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
},
"dashbord": {
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
- "comment_count": "Comentarios"
+ "no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
+ "flags": "Marcados",
+ "comment_count": "comentarios"
},
"streams": {
"empty_result": "No se encuentro articulo con esta busqueda. Tal vez extender la busqueda?",
diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css
index 09a94aa55..7bc1a21f4 100644
--- a/client/coral-embed-stream/src/Comment.css
+++ b/client/coral-embed-stream/src/Comment.css
@@ -1,3 +1,7 @@
.Reply {
position: relative;
}
+
+.Comment {
+
+}
diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js
index 7262d10ed..72449c906 100644
--- a/client/coral-embed-stream/src/Comment.js
+++ b/client/coral-embed-stream/src/Comment.js
@@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
+import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
import styles from './Comment.css';
@@ -25,6 +26,11 @@ const getActionSummary = (type, comment) => comment.action_summaries
.filter((a) => a.__typename === type)[0];
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
+// hold actions links (e.g. Like, Reply) along the comment footer
+const ActionButton = ({children}) => {
+ return
{ children };
+};
+
class Comment extends React.Component {
constructor(props) {
@@ -38,12 +44,12 @@ class Comment extends React.Component {
// id of currently opened ReplyBox. tracked in Stream.js
activeReplyBox: PropTypes.string.isRequired,
setActiveReplyBox: PropTypes.func.isRequired,
- refetch: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
postFlag: PropTypes.func.isRequired,
postLike: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string,
+ highlighted: PropTypes.string,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
@@ -74,7 +80,13 @@ class Comment extends React.Component {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
- }).isRequired
+ }).isRequired,
+
+ // dispatch action to add a tag to a comment
+ addCommentTag: React.PropTypes.func,
+
+ // dispatch action to remove a tag from a comment
+ removeCommentTag: React.PropTypes.func,
}
render () {
@@ -85,60 +97,105 @@ class Comment extends React.Component {
asset,
depth,
postItem,
- refetch,
addNotification,
showSignInDialog,
postLike,
+ highlighted,
postFlag,
postDontAgree,
loadMore,
setActiveReplyBox,
activeReplyBox,
- deleteAction
+ deleteAction,
+ addCommentTag,
+ removeCommentTag,
} = this.props;
const like = getActionSummary('LikeActionSummary', comment);
const flag = getActionSummary('FlagActionSummary', comment);
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
+ let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
+ commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
+
+ // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
+ const notifyOnError = (fn, errorToMessage) => async () => {
+ if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
+ try {
+ return await fn();
+ } catch (error) {
+ addNotification('error', errorToMessage(error));
+ throw error;
+ }
+ };
+
+ const addBestTag = notifyOnError(() => addCommentTag({
+ id: comment.id,
+ tag: BEST_TAG,
+ }), () => 'Failed to tag comment as best');
+
+ const removeBestTag = notifyOnError(() => removeCommentTag({
+ id: comment.id,
+ tag: BEST_TAG,
+ }), () => 'Failed to remove best comment tag');
return (