diff --git a/.gitignore b/.gitignore index 5a42acb67..c76651868 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist/coral-admin/bundle.js tests/e2e/reports .DS_Store *.iml +*.swp dump.rdb .env gaba.cfg diff --git a/README.md b/README.md index 0f217d89c..0b77e95b7 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,14 @@ A commenting platform from The Coral Project. [https://coralproject.net](https:/ ## Contributing to Talk +### Product Roadmap +You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625 + +You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk + ### Local Dependencies Node + Mongo ### Getting Started @@ -19,13 +25,19 @@ Runs Talk. The Talk application requires specific configuration options to be available inside the environment in order to run, those variables are listed here: -- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to - secure cookies +- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to +secure cookies. - `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook - Login enabled app. +Login enabled app. - `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your - Facebook Login enabled app. -- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `://` without the path. +Facebook Login enabled app. +- `TALK_ROOT_URL` (*required*) - root url of the installed application externally +available in the format: `://` without the path. +- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name. +- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using. +- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using. +- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`. +- `TALK_SMTP_PORT` (*required*) - SMTP port. ### Running with Docker Make sure you have Docker running first and then run `docker-compose up -d` @@ -37,9 +49,11 @@ Make sure you have Docker running first and then run `docker-compose up -d` `npm run lint` ### Helpful URLs -Bare comment stream: http://localhost:5000/client/coral-embed-stream/ -Comment stream embedded on sample article: http://localhost:5000/client/coral-embed-stream/samplearticle.html -Moderator view: http://localhost:5000/admin/ +Comment stream: http://localhost:3000/ + +Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html + +Moderator view: http://localhost:3000/admin ### Docs `swagger.yaml` diff --git a/app.js b/app.js index dd58e9883..fe392932b 100644 --- a/app.js +++ b/app.js @@ -94,7 +94,9 @@ app.use((req, res, next) => { // returning a status code that makes sense. app.use('/api', (err, req, res, next) => { if (err !== ErrNotFound) { - console.error(err); + if (app.get('env') !== 'test') { + console.error(err); + } } res.status(err.status || 500); diff --git a/architecture.png b/architecture.png new file mode 100644 index 000000000..0096ccd41 Binary files /dev/null and b/architecture.png differ diff --git a/architecture.xml b/architecture.xml new file mode 100644 index 000000000..ce2b09a19 --- /dev/null +++ b/architecture.xml @@ -0,0 +1 @@ +7Vpbc6M2FP41nrYP6wFksP1oO0k7nd1O2nTa7qMMCtZGICpE4vTXVwIJkCVnnQbbWU/z4MDRBen7zk0HRmCVbX9ksNh8ogkio8BLtiNwNQqC6dwTv1Lw3AjCYNoIUoaTRuR3gjv8D1JCNS6tcIJKoyOnlHBcmMKY5jmKuSGDjNEns9s9JeZTC5giS3AXQ2JL/8QJ3zTSWeh18p8QTjf6yb6nWtYwfkgZrXL1vFEA7uu/pjmDei7Vv9zAhD71ROB6BFaMUt5cZdsVIhJaDVsz7mZPa7tuhnJ+yIBJ1Ix4hKRCesn1wvizBkOMELiLm6VYbSGFMaGVmGD5tMEc3RUwlsInoQlCtuEZEXe+uFRTI8bRdu/6/HbXQpkQzRBnz6KLGhBpjdCK1Nw9daQEGvpNn5C56giVIqTtzB0Y4kLh4cYGOKBIhJqoW8r4hqY0h+S6ky5r7pGcwTPBQFvM/5LicajuPuuWXCys1yRvP6sJSg4ZX0iNrmGHZYljLb7BpJ08T+xOQtjr8gVx/qwsDVacClG3g4+UFqpfyRl9QCtKKKv3DLz6r23R9tARbLEpIKMVixVmCkWx4hSpXoHSOgnni1rAEIEcP5pW+RZOw/kxOPV7jHrjaWiQalA6Bt8IqTvU/BeWQ+9EpEb+8UkNTFLHfmTyGrbtt4hhsQXEviETHoDtJpKcgO3AiliLohCCO8REqBkFERErWib4UVym8vL7X0R6MgpWcp/bgqGyHH8pf9AdxeN6fS1FOijEOdCyot7eEAemZojzdZbRC3L+xBXkZt7b0Zx8Pf4fH4I2fVIQABcEMxcEkwEgiCbHdx8vhYTatVyil/AnjtB/Ii/huxTbcg2LJMP5SObIN+JXHmOYWAbN9zkHewLpet6JI/F1dqwdSTCzrShwWFE0hBGF5zWi6cUaUXhGIwotI1pRBmWX62yNkvIdaDmYOWLFsbTcn9vJRyWeGni3jD5i4T7OAUmkE2Bt+L59TPY9YEMCBoCkXW4HySeIiUrIsNj1OQAJDECmDhVxlQ2GwCME53WEl+oHdc3gLCfMwC6T/UzXwta9XyskJI5Dx0OF3tcxY7aTYx/qN6cDGAVwBBKCxWa+kxjeib1biIhtcRMEU8dymkvLuRe6uiOCBKe51GkxvzyFLyVIOIZkoRoynCS12blQ34Pra4qWhxxnpg6kgyGQjo7hfuyi5WW4mDe4DjX0lmIxY0t9sHOYB7ucNi5Njepoff1EjR+0Jqr1o93PYSWAo5TPTJVpg5Sjzn056mRGLDC3I1ZzLD1BxJrYdZz/X14ci9TwRKQC+/hxw2jtNq7zRJ5CSJXivHTlIwzBWHS8YSiptoemJfqFXxOLE1qt6xbf0BV/TyQd9k3g7rkvmJzw3DeZWsCrYpFQNFU+imAmd52vy8JdK1rRLKtyzJ/39f1qtekTzGHa1bgPJK/KyCKWC+0yoY9wjcgtLXFd7AJXa8o5zUQHIhuW7UvsniWp19h2NsXpDtntO21pd4V+GyKVA+epEg+gEMAMkD6w9QEcSx1mdjrbI9eiDWXrujLwrmkbnhLgOHcfixKdYvco+Q0l2C7HaHgTyGEp4N13Dhj4O4adigSIbGjmcxsa3x8AG/2sgXMBqyRxifE+1J8fnaH8GjpKazRP6dV+p3FqrTYNPnAWHl3v7QZR68CCx3C+PXyivyv5RdVSFhU+qPrAQvQg6J53rbuR975JrexpZMOHslZPOYs/Kbb2LLpS/jskD+LfgsWSj5hXjfgKw5TBrBcUmseZS3ghJTjz9v4QLWNfUkiKDXzlNi6p1mNmIbZbb2VvrDTL3L39LrA55nffXoLrfwE= \ No newline at end of file diff --git a/bin/cli-settings b/bin/cli-settings index c639a5ca5..e7ba30151 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -15,7 +15,7 @@ const mongoose = require('../mongoose'); const Setting = require('../models/setting'); const util = require('../util'); -// Regeister the shutdown criteria. +// Register the shutdown criteria. util.onshutdown([ () => mongoose.disconnect() ]); diff --git a/bin/cli-users b/bin/cli-users index 2e0dc84e6..012eba9d4 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -34,6 +34,7 @@ function createUser(options) { email: options.email, password: options.password, displayName: options.name, + role: options.role }); } @@ -62,6 +63,11 @@ function createUser(options) { name: 'displayName', description: 'Display Name', required: true + }, + { + name: 'role', + description: 'User Role', + required: false } ], (err, result) => { if (err) { @@ -76,15 +82,21 @@ function createUser(options) { }); }) .then((result) => { - return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim()); - }) - .then((user) => { - console.log(`Created user ${user.id}.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(); + return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim()) + .then((user) => { + console.log(`Created user ${user.id}.`); + + return User + .addRoleToUser(user.id, result.role.trim()) + .then(() => { + console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`); + util.shutdown(); + }); + }) + .catch((err) => { + console.error(err); + util.shutdown(); + }); }); } @@ -330,6 +342,7 @@ program .option('--email [email]', 'Email to use') .option('--password [password]', 'Password to use') .option('--name [name]', 'Name to use') + .option('--role [role]', 'Role to add') .option('-f, --flag_mode', 'Source from flags instead of prompting') .description('create a new user') .action(createUser); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index e5670d169..3774ed5f9 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -81,9 +81,11 @@ class ModerationQueue extends React.Component { singleView={singleView} commentIds={ comments.get('ids') - .filter(id => !comments.get('byId') + .filter(id => + comments + .get('byId') .get(id) - .get('status')) + .get('status') === 'premod') } comments={comments.get('byId')} users={users.get('byId')} diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 7f3b1b30f..f90b96201 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -67,23 +67,42 @@ class CommentStream extends Component { // Set up messaging between embedded Iframe an parent component this.pym = new Pym.Child({polling: 100}); - const path = /https?\:\/\/([^?#]+)/.exec(this.pym.parentUrl); + const path = this.pym.parentUrl.split('#')[0]; - this.props.getStream(path[1] || window.location); + this.props.getStream(path || window.location); this.path = path; this.pym.sendMessage('childReady'); + + this.pym.onMessage('DOMContentLoaded', hash => { + const commentId = hash.replace('#', 'c_'); + let count = 0; + const interval = setInterval(() => { + if (document.getElementById(commentId)) { + window.clearInterval(interval); + this.pym.scrollParentToChildEl(commentId); + } + + if (++count > 100) { // ~10 seconds + // give up waiting for the comments to load. + // it would be weird for the page to jump after that long. + window.clearInterval(interval); + } + }, 100); + }); } render () { const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0]; const rootItem = this.props.items.assets && this.props.items.assets[rootItemId]; const {actions, users, comments} = this.props.items; - const {loggedIn, user, showSignInDialog} = this.props.auth; + const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth; const {status, closedMessage} = this.props.config; const {activeTab} = this.state; - - return
+ const expandForLogin = showSignInDialog ? { + minHeight: document.body.scrollHeight + 150 + } : {}; + return
{ rootItem ?
@@ -116,7 +135,7 @@ class CommentStream extends Component {
:

{closedMessage}

} - {!loggedIn && } + {!loggedIn && } { rootItem.comments && rootItem.comments.map((commentId) => { const comment = comments[commentId]; @@ -272,7 +291,7 @@ const mapDispatchToProps = (dispatch) => ({ getStream: (rootId) => dispatch(getStream(rootId)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), - showSignInDialog: () => dispatch(showSignInDialog()), + showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)), deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)), appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 59efc3dc8..766ad763a 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -8,7 +8,7 @@ body { } .expandForSignin { - min-height: 550px; + min-height: 600px; } button { @@ -113,7 +113,6 @@ hr { /* Comment styles */ .comment { - position: relative; margin-bottom: 10px; position: relative; } diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 13a9c72bc..7ad5e4c34 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -6,7 +6,7 @@ import coralApi, {base} from '../helpers/response'; import {addItem} from './items'; // Dialog Actions -export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG}); +export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset}); export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG}); export const changeView = view => dispatch => diff --git a/client/coral-framework/modules/i18n/i18n.js b/client/coral-framework/modules/i18n/i18n.js index afe8606d1..1d81e885c 100644 --- a/client/coral-framework/modules/i18n/i18n.js +++ b/client/coral-framework/modules/i18n/i18n.js @@ -1,5 +1,7 @@ import timeago from 'timeago.js'; import esTA from '../../../../node_modules/timeago.js/locales/es'; +import has from 'lodash/has'; +import get from 'lodash/get'; /** * Default locales, this should be overriden by config file @@ -46,21 +48,18 @@ class i18n { * it takes a string with the translation key and returns * the translation value or the key itself if not found * it works with nested translations (my.page.title) + * + * any extra parameters are optional and replace a variable marked by {0}, {1}, etc in the translation. */ - this.t = (key) => { - const arr = key.split('.'); - let translation = this.translations; - try { - for (let i = 0; i < arr.length; i++) {translation = translation[arr[i]];} - } catch (error) { - console.warn(`${key} language key not set`); - return key; - } - - const val = String(translation); - if (val) { - return val; + this.t = (key, ...replacements) => { + if (has(this.translations, key)) { + let translation = get(this.translations, key); + // replace any {n} with the arguments passed to this method + replacements.forEach((str, i) => { + translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str); + }); + return translation; } else { console.warn(`${key} language key not set`); return key; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index a4803fa96..cf5b25921 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -22,7 +22,8 @@ export default function auth (state = initialState, action) { switch (action.type) { case actions.SHOW_SIGNIN_DIALOG : return state - .set('showSignInDialog', true); + .set('showSignInDialog', true) + .set('signInOffset', action.offset); case actions.HIDE_SIGNIN_DIALOG : return state.merge(Map({ isLoading: false, diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index f0ab1b22f..dd5979888 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -8,7 +8,8 @@ const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDial const flagged = flag && flag.current_user; const onFlagClick = () => { if (!currentUser) { - showSignInDialog(); + const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75; + showSignInDialog(offset); return; } if (!flagged) { diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index b3d31fbf4..0672da071 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -8,7 +8,8 @@ const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDial const liked = like && like.current_user; const onLikeClick = () => { if (!currentUser) { - showSignInDialog(); + const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75; + showSignInDialog(offset); return; } if (!liked) { diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 242b7443a..68d96b910 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -6,8 +6,14 @@ import SignInContent from './SignInContent'; import SingUpContent from './SignUpContent'; import ForgotContent from './ForgotContent'; -const SignDialog = ({open, view, handleClose, ...props}) => ( - +const SignDialog = ({open, view, handleClose, offset, ...props}) => ( + × {view === 'SIGNIN' && } {view === 'SIGNUP' && } diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 28dc021d2..07f82a849 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -129,7 +129,7 @@ class SignInContainer extends Component { } render() { - const {auth, showSignInDialog, noButton} = this.props; + const {auth, showSignInDialog, noButton, offset} = this.props; return (
{!noButton &&