mirror of
https://github.com/wassname/talk.git
synced 2026-07-14 11:18:50 +08:00
Merge branch 'master' into my-comments-ux
This commit is contained in:
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
|
||||
@@ -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()));
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
: null }
|
||||
<PubDate created_at={comment.created_at} />
|
||||
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<ActionButton>
|
||||
@@ -166,13 +169,16 @@ class Comment extends React.Component {
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
</ActionButton>
|
||||
{
|
||||
!disableReply &&
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
</ActionButton>
|
||||
}
|
||||
<ActionButton>
|
||||
<IfUserCanModifyBest user={currentUser}>
|
||||
<BestButton
|
||||
@@ -217,6 +223,7 @@ class Comment extends React.Component {
|
||||
comment.replies.map(reply => {
|
||||
return <Comment
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
parentId={comment.id}
|
||||
@@ -232,7 +239,7 @@ class Comment extends React.Component {
|
||||
removeCommentTag={removeCommentTag}
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
key={`${reply.id}:${depth}`}
|
||||
key={reply.id}
|
||||
comment={reply} />;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ class Embed extends Component {
|
||||
/>
|
||||
<div className="embed__stream">
|
||||
<Stream
|
||||
open={openStream}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
@@ -224,7 +225,7 @@ class Embed extends Component {
|
||||
topLevel={true}
|
||||
assetId={asset.id}
|
||||
comments={asset.comments}
|
||||
moreComments={asset.commentCount > asset.comments.length}
|
||||
moreComments={countCache[asset.id] > asset.comments.length}
|
||||
loadMore={this.props.loadMore}/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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 =>
|
||||
<Comment
|
||||
disableReply={!open}
|
||||
setActiveReplyBox={this.props.setActiveReplyBox}
|
||||
activeReplyBox={this.props.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
|
||||
@@ -3,6 +3,8 @@ import STREAM_QUERY from './streamQuery.graphql';
|
||||
import LOAD_MORE from './loadMore.graphql';
|
||||
import GET_COUNTS from './getCounts.graphql';
|
||||
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
function getQueryVariable(variable) {
|
||||
let query = window.location.search.substring(1);
|
||||
@@ -39,7 +41,7 @@ export const getCounts = (data) => ({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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+20
-21
@@ -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]) => {
|
||||
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user