diff --git a/PLUGINS.md b/PLUGINS.md index 20ba789be..a77b0b8b2 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -149,7 +149,7 @@ This will merge with the existing resolvers in core and from previous plugins. { RootMutation: { createPerson: { - post: async (obj, args, {plugins: {Slack}}, person) { + post: async (obj, args, {plugins: {Slack}}, info, person) { if (!person) { return person; } @@ -248,7 +248,7 @@ module.exports = { hooks: { RootMutation: { createPerson: { - post: async (obj, args, {plugins: {Slack}}, person) => { + post: async (obj, args, {plugins: {Slack}}, info, person) => { if (!person) { return person; } diff --git a/README.md b/README.md index 64fe2739c..7e303f70e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Facebook Login enabled app. - `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`. - `TALK_SMTP_PORT` (*required for email*) - SMTP port. - `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint. +- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout. +- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout. Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for alternative methods of loading configuration during development. diff --git a/client/coral-admin/src/containers/Stories/Stories.js b/client/coral-admin/src/containers/Stories/Stories.js index 4d2ad086a..1264663f0 100644 --- a/client/coral-admin/src/containers/Stories/Stories.js +++ b/client/coral-admin/src/containers/Stories/Stories.js @@ -2,13 +2,14 @@ import React, {Component} from 'react'; import styles from './Stories.css'; import {connect} from 'react-redux'; import I18n from 'coral-framework/modules/i18n/i18n'; -import {fetchAssets, updateAssetState} from '../../actions/assets'; -import translations from '../../translations.json'; +import {fetchAssets, updateAssetState} from 'coral-admin/src/actions/assets'; +import translations from 'coral-admin/src/translations.json'; import {Link} from 'react-router'; import {Pager, Icon} from 'coral-ui'; import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl'; import EmptyCard from 'coral-admin/src/components/EmptyCard'; +import sortBy from 'lodash/sortBy'; const limit = 25; @@ -104,7 +105,11 @@ class Stories extends Component { const {search, sort, filter} = this.state; const {assets} = this.props; - const assetsIds = assets.ids.map((id) => assets.byId[id]); + const assetsIds = sortBy(assets.ids.map((id) => assets.byId[id]), 'publication_date'); + + if (this.state.sort === 'desc') { + assetsIds.reverse(); + } return (
diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js index c9a82f1c5..03f0be9bb 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/assets.js @@ -23,8 +23,13 @@ export default function assets (state = initialState, action) { } const replaceAssets = (action, state) => { - const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); - return state.set('byId', assets) - .set('count', action.count) - .set('ids', List(assets.keys())); + const assets = fromJS(action.assets.reduce((prev, curr) => { + prev[curr.id] = curr; + return prev; + }, {})); + + return state + .set('byId', assets) + .set('count', action.count) + .set('ids', List(assets.keys())); }; diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 06352ec2f..caffb6894 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -43,6 +43,7 @@ class Comment extends React.Component { // id of currently opened ReplyBox. tracked in Stream.js activeReplyBox: PropTypes.string.isRequired, + disableReply: PropTypes.bool, setActiveReplyBox: PropTypes.func.isRequired, showSignInDialog: PropTypes.func.isRequired, postFlag: PropTypes.func.isRequired, @@ -109,6 +110,7 @@ class Comment extends React.Component { deleteAction, addCommentTag, removeCommentTag, + disableReply, } = this.props; const like = getActionSummary('LikeActionSummary', comment); @@ -155,6 +157,7 @@ class Comment extends React.Component { ? : null } +
@@ -166,13 +169,16 @@ class Comment extends React.Component { showSignInDialog={showSignInDialog} currentUser={currentUser} /> - - setActiveReplyBox(comment.id)} - parentCommentId={parentId || comment.id} - currentUserId={currentUser && currentUser.id} - banned={false} /> - + { + !disableReply && + + setActiveReplyBox(comment.id)} + parentCommentId={parentId || comment.id} + currentUserId={currentUser && currentUser.id} + banned={false} /> + + } { return ; }) } diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index ce6dc638a..4722a44b1 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -202,6 +202,7 @@ class Embed extends Component { />
asset.comments.length} + moreComments={countCache[asset.id] > asset.comments.length} loadMore={this.props.loadMore}/> diff --git a/client/coral-embed-stream/src/LoadMore.js b/client/coral-embed-stream/src/LoadMore.js index bf89793d0..942a53b27 100644 --- a/client/coral-embed-stream/src/LoadMore.js +++ b/client/coral-embed-stream/src/LoadMore.js @@ -7,18 +7,17 @@ const lang = new I18n(translations); const loadMoreComments = (assetId, comments, loadMore, parentId) => { - if (!comments.length) { - return; + let cursor = null; + if (comments.length) { + cursor = parentId + ? comments[0].created_at + : comments[comments.length - 1].created_at; } - const cursor = parentId - ? comments[0].created_at - : comments[comments.length - 1].created_at; - loadMore({ limit: ADDTL_COMMENTS_ON_LOAD_MORE, cursor, - assetId, + asset_id: assetId, parent_id: parentId, sort: parentId ? 'CHRONOLOGICAL' : 'REVERSE_CHRONOLOGICAL' }); diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index 7743f3f1c..defca1a61 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -8,6 +8,7 @@ class Stream extends React.Component { addNotification: PropTypes.func.isRequired, postItem: PropTypes.func.isRequired, asset: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, comments: PropTypes.array.isRequired, currentUser: PropTypes.shape({ username: PropTypes.string, @@ -15,10 +16,10 @@ class Stream extends React.Component { }), // dispatch action to add a tag to a comment - addCommentTag: React.PropTypes.func, + addCommentTag: PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: React.PropTypes.func, + removeCommentTag: PropTypes.func, } constructor(props) { @@ -55,6 +56,7 @@ class Stream extends React.Component { addNotification, postFlag, postLike, + open, postDontAgree, loadMore, deleteAction, @@ -68,6 +70,7 @@ class Stream extends React.Component { { comments.map(comment => ({asset_id, limit, sort}) => { }); }; -export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, newComments) => { +export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => { return data.fetchMore({ query: LOAD_MORE, variables: { @@ -60,10 +62,18 @@ export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, n ...oldData, asset: { ...oldData.asset, - comments: oldData.asset.comments.map((comment) => - comment.id === parent_id - ? {...comment, replies: [...comment.replies, ...new_top_level_comments]} - : comment) + comments: oldData.asset.comments.map(comment => { + + // since the dipslayed replies and the returned replies can overlap, + // pull out the unique ones. + const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id'); + + // since we just gave the returned replies precedence, they're now out of order. + // resort according to date. + return comment.id === parent_id + ? {...comment, replies: sortBy(uniqueReplies, 'created_at')} + : comment; + }) } }; } else { diff --git a/routes/api/account/index.js b/routes/api/account/index.js index ee10636bf..44b4b3953 100644 --- a/routes/api/account/index.js +++ b/routes/api/account/index.js @@ -59,8 +59,7 @@ router.post('/password/reset', (req, res, next) => { } return mailer.sendSimple({ - app: req.app, // needed to render the templates. - template: 'email/password-reset', // needed to know which template to render! + template: 'password-reset', // needed to know which template to render! locals: { // specifies the template locals. token, rootURL: process.env.TALK_ROOT_URL diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 2af6bc2ee..a01fed2c9 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -76,8 +76,7 @@ router.post('/:user_id/email', authorization.needed('ADMIN'), (req, res, next) = if (localProfile) { const options = { - app: req.app, // needed to render the templates. - template: 'email/notification', // needed to know which template to render! + template: 'notification', // needed to know which template to render! locals: { // specifies the template locals. body: req.body.body }, @@ -106,8 +105,7 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService .createEmailConfirmToken(userID, email, referer) .then((token) => { return mailer.sendSimple({ - app, // needed to render the templates. - template: 'email/email-confirm', // needed to know which template to render! + template: 'email-confirm', // needed to know which template to render! locals: { // specifies the template locals. token, rootURL: process.env.TALK_ROOT_URL, diff --git a/views/email/email-confirm.ejs b/services/email/email-confirm.ejs similarity index 100% rename from views/email/email-confirm.ejs rename to services/email/email-confirm.ejs diff --git a/views/email/email-confirm.txt.ejs b/services/email/email-confirm.txt.ejs similarity index 100% rename from views/email/email-confirm.txt.ejs rename to services/email/email-confirm.txt.ejs diff --git a/views/email/notification.ejs b/services/email/notification.ejs similarity index 100% rename from views/email/notification.ejs rename to services/email/notification.ejs diff --git a/views/email/notification.txt.ejs b/services/email/notification.txt.ejs similarity index 100% rename from views/email/notification.txt.ejs rename to services/email/notification.txt.ejs diff --git a/views/email/password-reset.ejs b/services/email/password-reset.ejs similarity index 100% rename from views/email/password-reset.ejs rename to services/email/password-reset.ejs diff --git a/views/email/password-reset.txt.ejs b/services/email/password-reset.txt.ejs similarity index 100% rename from views/email/password-reset.txt.ejs rename to services/email/password-reset.txt.ejs diff --git a/services/mailer.js b/services/mailer.js index 6615ee9dc..2928570ad 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -1,6 +1,9 @@ const debug = require('debug')('talk:services:mailer'); const nodemailer = require('nodemailer'); const kue = require('./kue'); +const path = require('path'); +const fs = require('fs'); +const _ = require('lodash'); const smtpRequiredProps = [ 'TALK_SMTP_FROM_ADDRESS', @@ -13,6 +16,20 @@ if (smtpRequiredProps.some(prop => !process.env[prop])) { console.error(`${smtpRequiredProps.join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`); } +// load all the templates as strings +const templateStrings = {}; +fs.readdir(path.join(__dirname, 'email'), (err, files) => { + if (err) { + throw err; + } + + files.forEach(file => { + fs.readFile(path.join(__dirname, 'email', file), 'utf8', (err, data) => { + templateStrings[file] = _.template(data); + }); + }); +}); + const options = { host: process.env.TALK_SMTP_HOST, auth: { @@ -38,25 +55,7 @@ const mailer = module.exports = { name: 'mailer' }), - /** - * Render renders the template with the given locals and returns the rendered - * html/text. - */ - render(app, template, locals = {}) { - return new Promise((resolve, reject) => { - - // Render the template with the app.render method. - app.render(template, locals, (err, rendered) => { - if (err) { - return reject(err); - } - - return resolve(rendered); - }); - }); - }, - - sendSimple({app, template, locals, to, subject}) { + sendSimple({template, locals, to, subject}) { if (!to) { return Promise.reject('sendSimple requires a comma-separated list of "to" addresses'); } @@ -71,10 +70,10 @@ const mailer = module.exports = { return Promise.all([ // Render the HTML version of the email. - mailer.render(app, `${template}.ejs`, locals), + templateStrings[`${template}.ejs`](locals), // Render the TEXT version of the email. - mailer.render(app, `${template}.txt.ejs`, locals) + templateStrings[`${template}.txt.ejs`](locals) ]) .then(([html, text]) => { diff --git a/services/passport.js b/services/passport.js index 4ec80bb60..f1994392f 100644 --- a/services/passport.js +++ b/services/passport.js @@ -116,14 +116,15 @@ const CheckIfNeedsRecaptcha = (user, email) => { * This stores the Recaptcha secret. */ const RECAPTCHA_SECRET = process.env.TALK_RECAPTCHA_SECRET; +const RECAPTCHA_PUBLIC = process.env.TALK_RECAPTCHA_PUBLIC; /** * This is true when the recaptcha secret is provided and the Recaptcha feature * is to be enabled. */ -const RECAPTCHA_ENABLED = RECAPTCHA_SECRET && RECAPTCHA_SECRET.length > 0; +const RECAPTCHA_ENABLED = RECAPTCHA_SECRET && RECAPTCHA_SECRET.length > 0 && RECAPTCHA_PUBLIC && RECAPTCHA_PUBLIC.length > 0; if (!RECAPTCHA_ENABLED) { - console.log('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET to enable Recaptcha.'); + console.log('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'); } /**