Merge branch 'master' into status-history

This commit is contained in:
Wyatt Johnson
2016-12-05 14:20:31 -05:00
64 changed files with 1041 additions and 506 deletions
+2 -1
View File
@@ -1 +1,2 @@
dist
dist
client/lib
+95
View File
@@ -0,0 +1,95 @@
# Contribution Guide
We're very excited that you're interested in contributing to Talk! There is much to do. Before you begin, please review this document to get a sense of the practices and philosophies that hold this project together.
## Doing the Work
We are here to make it as seamless as possible to contribute to Talk. The following lists are meant to make it straightforward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content.
### Code Reviews
One of the most valuable aspects of working in software. It is something that should challenge the reviewer and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants.
Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand.
Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace.
**Is the code clear, efficient and a pleasure to read?**
Somewhere at the intersection of good variable names, well laid out file structures, consistent formatting and appropriate comments lies beautiful code. Code is language spoken to at least two very distinct audiences, the computer that interprets it and the developer who encounters it. Both should be at the front of your mind when reviewing code.
Thinking like a computer, you could ask:
* Is the code using memory efficiently?
* Is data being moved around unnecessarily?
* Are multiple network requests being made where fewer would do?
* Is there excess processing happening in a synchronous flow that may disrupt user experience?
* Are there large libraries included for small gains?
Then, returning to your human roots... Is the code readable?
* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward?
* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices?
* Are the files separated logically such that each one contains a clear concept of code?
**Is the API documentation up to date? Are all client calls written against the docs?**
We use [swagger](https://github.com/coralproject/talk/blob/master/swagger.yaml) to track our API documentation.
* If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs.
* If client code calls APIs, are they written against the swagger.yml file? Are all return codes handled?
**Is there sufficient test coverage?**
Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests)
* Can you a sense of the logic behind the code by reading the tests?
* Can you see both what should happen and what should _never, ever_ be allowed to happen?
* Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)?
### Forking, Branching and Merging
Talk follows the _master as tip_ repo structure. `master` is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added.
Releases are _tagged_ off the master branch.
Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution.
* At the outset of a piece of work, a branch or fork is made from master.
* The work is done in that fork.
* As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.)
* At least one other contributor to the project must review all code (see Code Reviews below.)
* If there are merge conflicts with master, merge master into the branch.
* Ensure that [circleci](https://circleci.com/) passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.)
* If merge conflicts exist with `master`, merge `master` into your branch and re-run CI before merging into master.
* Merge to master, but _you're not quite done yet!_
* Deploy master to staging (or have a core member do so.)
* Ensure that all your changes are working on staging.
* Have your reviewer verify the same.
* ... aaaand the work is delivered!
## Continuous Integration
We use circleci to run our ci: [https://circleci.com/gh/coralproject/talk](https://circleci.com/gh/coralproject/talk)
Our pipeline will _test_, _lint_, and _build_ all pushes to the repo.
Any branch not passing CI will not be merged into master.
If you're working in a fork, please run each of the steps locally before submitting a PR.
## Coding Style
### API Design
When building APIs, we follow these principles:
* Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations.
* Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object.
* Put non-required, flexible variables into query params, required/identity based values in request params.
+3 -5
View File
@@ -1,5 +1,5 @@
import * as actions from '../constants/auth';
import {base, handleResp, getInit} from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/response';
// Check Login
@@ -9,8 +9,7 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
fetch(`${base}/auth`, getInit('GET'))
.then(handleResp)
coralApi('/auth')
.then(user => {
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
dispatch(checkLoginSuccess(user, isAdmin));
@@ -26,8 +25,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
fetch(`${base}/auth`, getInit('DELETE'))
.then(handleResp)
coralApi('/auth', {method: 'DELETE'})
.then(() => dispatch(logOutSuccess()))
.catch(error => dispatch(logOutFailure(error)));
};
+3 -4
View File
@@ -9,12 +9,11 @@ import {
SET_ROLE
} from '../constants/community';
import {base, getInit, handleResp} from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/response';
export const fetchCommenters = (query = {}) => dispatch => {
dispatch(requestFetchCommenters());
fetch(`${base}/user?${qs.stringify(query)}`, getInit('GET'))
.then(handleResp)
coralApi(`/user?${qs.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({
type: FETCH_COMMENTERS_SUCCESS,
@@ -42,7 +41,7 @@ export const newPage = () => ({
});
export const setRole = (id, role) => dispatch => {
return fetch(`${base}/user/${id}/role`, getInit('POST', {role}))
return coralApi(`/user/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
+3 -5
View File
@@ -1,4 +1,4 @@
import {base, handleResp, getInit} from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/response';
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
@@ -12,8 +12,7 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
export const fetchSettings = () => dispatch => {
dispatch({type: SETTINGS_LOADING});
fetch(`${base}/settings`, getInit('GET'))
.then(handleResp)
coralApi('/settings')
.then(settings => {
dispatch({type: SETTINGS_RECEIVED, settings});
})
@@ -29,8 +28,7 @@ export const updateSettings = settings => {
export const saveSettingsToServer = () => (dispatch, getState) => {
const settings = getState().settings.toJS().settings;
dispatch({type: SAVE_SETTINGS_LOADING});
fetch(`${base}/settings`, getInit('PUT', settings))
.then(handleResp)
coralApi('/settings', {method: 'PUT', body: settings})
.then(() => {
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
})
+8 -7
View File
@@ -12,26 +12,27 @@ const linkify = new Linkify();
// Render a single comment for the list
export default props => {
const links = linkify.getMatches(props.comment.get('body'));
const {comment, author} = props;
const links = linkify.getMatches(comment.get('body'));
return (
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<i className={`material-icons ${styles.avatar}`}>person</i>
<span>{props.comment.get('name') || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{props.comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
<span>{author.get('displayName') || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
</div>
<div>
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={styles.actions}>
{props.actions.map((action, i) => canShowAction(action, props.comment) ? (
{props.actions.map((action, i) => canShowAction(action, comment) ? (
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
cStyle={action}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment.get('id'))}
/>
) : null)}
</div>
@@ -40,7 +41,7 @@ export default props => {
<div className={styles.itemBody}>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
{props.comment.get('body')}
{comment.get('body')}
</Linkify>
</span>
</div>
@@ -112,13 +112,15 @@ export default class CommentList extends React.Component {
}
render () {
const {singleView, commentIds, comments, hideActive, key} = this.props;
const {singleView, commentIds, comments, users, hideActive, key} = this.props;
const {active} = this.state;
return (
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
{commentIds.map((commentId, index) => (
<Comment comment={comments.get(commentId)}
{commentIds.map((commentId, index) => {
const comment = comments.get(commentId);
return <Comment comment={comment}
author={users.get(comment.get('author_id'))}
ref={el => { if (el && commentId === active) { this._active = el; } }}
key={index}
index={index}
@@ -126,8 +128,8 @@ export default class CommentList extends React.Component {
actions={this.props.actions}
actionsMap={actions}
isActive={commentId === active}
hideActive={hideActive} />
)).toArray()}
hideActive={hideActive} />;
}).toArray()}
</ul>
);
}
@@ -40,7 +40,7 @@ class CommentStream extends React.Component {
}
// Render the comment box along with the CommentList
render ({comments}, {snackbar, snackbarMsg}) {
render ({comments, users}, {snackbar, snackbarMsg}) {
return (
<div className={styles.container}>
<CommentBox onSubmit={this.onSubmit} />
@@ -48,6 +48,7 @@ class CommentStream extends React.Component {
singleView={false}
commentIds={comments.get('ids')}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={this.onClickAction}
actions={['flag']}
loading={comments.loading} />
@@ -57,4 +58,4 @@ class CommentStream extends React.Component {
}
}
export default connect(({comments}) => ({comments}))(CommentStream);
export default connect(({comments, users}) => ({comments, users}))(CommentStream);
@@ -61,7 +61,7 @@ class ModerationQueue extends React.Component {
// Render the tabbed lists moderation queues
render () {
const {comments} = this.props;
const {comments, users} = this.props;
const {activeTab, singleView, modalOpen} = this.state;
return (
@@ -82,10 +82,11 @@ class ModerationQueue extends React.Component {
commentIds={
comments.get('ids')
.filter(id => !comments.get('byId')
.get(id)
.get('status'))
.get(id)
.get('status'))
}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['reject', 'approve']}
loading={comments.loading} />
@@ -104,6 +105,7 @@ class ModerationQueue extends React.Component {
.get('status') === 'rejected')
}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['approve']}
loading={comments.loading} />
@@ -117,6 +119,7 @@ class ModerationQueue extends React.Component {
return !data.get('status') && data.get('flagged') === true;
})}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['reject', 'approve']}
loading={comments.loading} />
@@ -129,6 +132,6 @@ class ModerationQueue extends React.Component {
}
}
export default connect(({comments}) => ({comments}))(ModerationQueue);
export default connect(({comments, users}) => ({comments, users}))(ModerationQueue);
const lang = new I18n(translations);
@@ -1,30 +0,0 @@
export const base = '/api/v1';
export const getInit = (method, body) => {
let init = {
method,
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
}),
credentials: 'same-origin'
};
if (method.toLowerCase() !== 'get') {
init.body = JSON.stringify(body);
}
return init;
};
export const handleResp = res => {
if (res.status === 401) {
throw new Error('Not Authorized to make this request');
} else if (res.status > 399) {
throw new Error('Error! Status ', res.status);
} else if (res.status === 204) {
return res.text();
} else {
return res.json();
}
};
+1 -4
View File
@@ -24,10 +24,7 @@ export default function auth (state = initialState, action) {
.set('isAdmin', action.isAdmin)
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return state
.set('loggedIn', false)
.set('user', null)
.set('isAdmin', false);
return initialState;
default :
return state;
}
+3 -2
View File
@@ -2,6 +2,7 @@ import {combineReducers} from 'redux';
import comments from 'reducers/comments';
import settings from 'reducers/settings';
import community from 'reducers/community';
import users from 'reducers/users';
import auth from 'reducers/auth';
// Combine all reducers into a main one
@@ -9,6 +10,6 @@ export default combineReducers({
settings,
comments,
community,
auth
auth,
users
});
+20
View File
@@ -0,0 +1,20 @@
import {Map, List, fromJS} from 'immutable';
const initialState = Map({
byId: Map(),
ids: List()
});
export default (state = initialState, action) => {
switch (action.type) {
case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
default: return state;
}
};
// Replace the comment list with a new one
const replaceUsers = (action, state) => {
const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
return state.set('byId', users)
.set('ids', List(users.keys()));
};
+31 -18
View File
@@ -1,4 +1,4 @@
import {base, handleResp, getInit} from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/response';
/**
* The adapter is a redux middleware that interecepts the actions that need
@@ -15,9 +15,6 @@ export default store => next => action => {
case 'COMMENTS_MODERATION_QUEUE_FETCH':
fetchModerationQueueComments(store);
break;
// case 'COMMENT_STREAM_FETCH':
// fetchCommentStream(store);
// break;
case 'COMMENT_UPDATE':
updateComment(store, action.comment);
break;
@@ -33,24 +30,41 @@ export default store => next => action => {
const fetchModerationQueueComments = store =>
Promise.all([
fetch(`${base}/queue/comments/pending`, getInit('GET')),
fetch(`${base}/comments?status=rejected`, getInit('GET')),
fetch(`${base}/comments?action_type=flag`, getInit('GET'))
coralApi('/queue/comments/pending'),
coralApi('/comments?status=rejected'),
coralApi('/comments?action_type=flag')
])
.then(res => Promise.all(res.map(handleResp)))
.then(res => {
res[2] = res[2].map(comment => { comment.flagged = true; return comment; });
return res.reduce((prev, curr) => prev.concat(curr), []);
.then(([pending, rejected, flagged]) => {
/* Combine seperate calls into a single object */
let all = {};
all.comments = pending.comments
.concat(rejected.comments)
.concat(flagged.comments.map(comment => {
comment.flagged = true;
return comment;
}));
all.users = pending.users
.concat(rejected.users)
.concat(flagged.users);
all.actions = pending.actions
.concat(rejected.actions)
.concat(flagged.actions);
return all;
})
.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: res}))
.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
.then(all => {
/* Post comments and users to redux store. Actions will be posted when they are needed. */
store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS',
users: all.users});
store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: all.comments});
});
// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
// Update a comment. Now to update a comment we need to send back the whole object
const updateComment = (store, comment) => {
fetch(`${base}/comments/${comment.get('id')}/status`, getInit('PUT', {status: comment.get('status')}))
.then(handleResp)
coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}})
.then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res}))
.catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error}));
};
@@ -63,8 +77,7 @@ const createComment = (store, name, comment) => {
name: name,
createdAt: Date.now()
};
return fetch(`${base}/comments`, getInit('POST', body))
.then(handleResp)
return coralApi('/comments', {method: 'POST', body})
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
};
@@ -7,7 +7,7 @@
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut lobortis sollicitudin eros a ornare. Curabitur dignissim vestibulum massa non rhoncus. Cras laoreet ante vel nunc hendrerit, ac imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Pellentesque interdum nec elit sed tincidunt. Donec volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non enim nec neque congue faucibus porttitor sit amet dui.</p>
<p>Nunc pharetra orci id diam feugiat, vitae rutrum magna efficitur. Morbi porttitor blandit lorem, et facilisis tellus luctus at. Morbi tincidunt eget nisl id placerat. Nullam consectetur quam vel mauris lacinia, non consectetur est faucibus. Duis cursus auctor nulla nec sagittis. Aenean sem erat, ultrices a hendrerit consectetur, accumsan non lorem. Integer ac neque sed magna sodales vulputate at quis neque. Praesent eget ornare lacus. Donec ultricies, dolor eget commodo faucibus, arcu velit ullamcorper tellus, in cursus tellus elit sed urna. Suspendisse in consequat magna. Duis vel ullamcorper tortor, vel cursus libero. Proin et nisi luctus ligula faucibus luctus. Morbi pulvinar, justo ac feugiat elementum, libero tellus congue justo, pharetra ultrices felis felis id leo. Integer mattis quam tempus libero porta, ac pretium ligula elementum.</p>
<div id='coralStreamEmbed'></div>
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
<script type='text/javascript' src='/client/js/lib/pym.v1.min.js'></script>
<script>
var pymParent = new pym.Parent('coralStreamEmbed', 'index.html', {title: 'comments'});
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})</script>
+155 -147
View File
@@ -1,11 +1,14 @@
import React, {Component, PropTypes} from 'react';
import Pym from 'pym.js';
import {connect} from 'react-redux';
import {
itemActions,
Notification,
notificationActions,
authActions,
} from '../../coral-framework';
import {connect} from 'react-redux';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
import InfoBox from '../../coral-plugin-infobox/InfoBox';
import Content from '../../coral-plugin-commentcontent/CommentContent';
@@ -13,43 +16,19 @@ import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import Pym from 'pym.js';
import FlagButton from '../../coral-plugin-flags/FlagButton';
import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import UserBox from '../../coral-sign-in/components/UserBox';
import {TabBar, Tab, TabContent} from '../../coral-ui';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout} = authActions;
const mapStateToProps = (state) => {
return {
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS()
};
};
const mapDispatchToProps = (dispatch) => ({
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
deleteAction: (item, action, user, itemType) => {
return dispatch(deleteAction(item, action, user, itemType));
},
appendItemArray: (item, property, value, addToFront, itemType) =>
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
logout: () => dispatch(logout()),
});
const {logout, showSignInDialog} = authActions;
class CommentStream extends Component {
@@ -89,17 +68,11 @@ class CommentStream extends Component {
render () {
if (Object.keys(this.props.items).length === 0) {
// Loading mock asset
// Loading mock asset
this.props.postItem({
comments: [],
url: 'http://coralproject.net'
}, 'asset', 'assetTest');
// Loading mock user
//this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
// .then((id) => {
// this.props.setLoggedInUser(id);
// });
}
// TODO: Replace teststream id with id from params
@@ -109,72 +82,75 @@ class CommentStream extends Component {
const {actions, users, comments} = this.props.items;
const {loggedIn, user, showSignInDialog} = this.props.auth;
const {activeTab} = this.state;
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
{
rootItem
? <div>
? <div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
<Tab>Settings</Tab>
</TabBar>
<TabContent show={activeTab === 0}>
<div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
author={user}
/>
{!loggedIn && <SignInContainer />}
</div>
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
{/* Add to the restricted param a boolean if the user is suspended*/}
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
<TabContent show={activeTab === 0}>
<div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
author={user}
/>
{!loggedIn && <SignInContainer />}
</div>
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
showSignInDialog={this.props.showSignInDialog}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
comment_id={commentId}
asset_id={comment.asset_id}/>
</div>
commentId={commentId}
articleURL={this.path}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
@@ -189,72 +165,104 @@ class CommentStream extends Component {
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId}>
return <div className="reply" key={replyId} id={`c_${replyId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
comment_id={reply.parent_id}
asset_id={rootItemId}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
<ReplyButton
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
showReply={reply.showReply}/>
</div>;
})
}
</div>;
})
}
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer/>
</TabContent>
}
</div>;
})
}
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.handleSignInDialog}
/>
{!loggedIn && <SignInContainer noButton/>}
</TabContent>
</RestrictedContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
: 'Loading'
:
<Spinner/>
}
</div>;
}
}
const mapStateToProps = state => ({
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS()
});
const mapDispatchToProps = (dispatch) => ({
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
showSignInDialog: () => dispatch(showSignInDialog()),
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)),
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
logout: () => dispatch(logout()),
});
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
+44 -5
View File
@@ -56,12 +56,14 @@ hr {
/* Info Box Styles */
.coral-plugin-infobox-info {
position: fixed;
top: 0;
border: 0;
background: rgb(105,105,105);
color: white;
border-radius: 2px;
width: 100%;
text-align: center;
padding: 10px;
margin-bottom: 10px;
font-weight: bold;
display: block;
}
@@ -71,6 +73,11 @@ hr {
display: none;
}
.commentStream {
/* prevent absolutely positioned final permalink popover from being clipped */
padding-bottom: 50px;
}
/* Comment Box Styles */
.coral-plugin-commentbox-container {
display: flex;
@@ -79,6 +86,7 @@ hr {
.coral-plugin-commentbox-textarea {
flex: 1;
padding: 5px;
min-height: 100px;
}
.coral-plugin-commentbox-button-container {
@@ -105,7 +113,9 @@ hr {
/* Comment styles */
.comment {
position: relative;
margin-bottom: 10px;
position: relative;
}
.coral-plugin-commentcontent-text {
@@ -139,9 +149,8 @@ hr {
width: 50%;
}
.commentActionsLeft .material-icons,.commentActionsRight .material-icons,
.replyActionsLeft .material-icons, .replyActionsRight .material-icons {
font-size: 12px;
.material-icons {
font-size: 12px !important;
margin-left: 3px;
vertical-align: middle;
}
@@ -158,3 +167,33 @@ hr {
color: #CCC;
display: inline-block;
}
.coral-plugin-permalinks-container {
/*position: relative;*/
z-index: 2;
}
.coral-plugin-permalinks-popover {
display: none;
background-color: white;
border: 1px solid black;
width: calc(100% - 15px);
position: absolute;
top: 70px;
right: 0;
padding: 5px;
}
.coral-plugin-permalinks-popover.active {
display: block;
}
.coral-plugin-permalinks-copy-field {
display: block;
width: calc(100% - 5px);
}
.coral-plugin-permalinks-copied-text {
float: right;
margin: 8px;
}
+9 -14
View File
@@ -2,7 +2,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
import * as actions from '../constants/auth';
import {base, handleResp, getInit} from '../helpers/response';
import coralApi, {base} from '../helpers/response';
import {addItem} from './items';
// Dialog Actions
@@ -25,8 +25,7 @@ const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
export const fetchSignIn = (formData) => dispatch => {
dispatch(signInRequest());
fetch(`${base}/auth/local`, getInit('POST', formData))
.then(handleResp)
coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
dispatch(hideSignInDialog());
dispatch(signInSuccess(user));
@@ -74,8 +73,7 @@ const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = formData => dispatch => {
dispatch(signUpRequest());
fetch(`${base}/user`, getInit('POST', formData))
.then(handleResp)
coralApi('/user', {method: 'POST', body: formData})
.then(({user}) => {
dispatch(signUpSuccess(user));
setTimeout(() =>{
@@ -93,8 +91,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU
export const fetchForgotPassword = email => dispatch => {
dispatch(forgotPassowordRequest(email));
fetch(`${base}/user/request-password-reset`, getInit('POST', {email}))
.then(handleResp)
coralApi('/user/request-password-reset', {method: 'POST', body: {email}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
};
@@ -107,8 +104,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
fetch(`${base}/auth`, getInit('DELETE'))
.then(handleResp)
coralApi('/auth', {method: 'DELETE'})
.then(() => dispatch(logOutSuccess()))
.catch(error => dispatch(logOutFailure(error)));
};
@@ -126,14 +122,13 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
fetch(`${base}/auth`, getInit('GET'))
.then((res) => {
if (res.status !== 200) {
coralApi('/auth')
.then(user => {
if (!user) {
throw new Error('not logged in');
}
return res.json();
dispatch(checkLoginSuccess(user));
})
.then(user => dispatch(checkLoginSuccess(user)))
.catch(error => dispatch(checkLoginFailure(error)));
};
+6 -11
View File
@@ -1,4 +1,4 @@
import {getInit, base, handleResp} from '../helpers/response';
import coralApi from '../helpers/response';
import {fromJS} from 'immutable';
/* Item Actions */
@@ -95,8 +95,7 @@ export const appendItemArray = (id, property, value, add_to_front, item_type) =>
*/
export function getStream (assetUrl) {
return (dispatch) => {
return fetch(`${base}/stream?asset_url=${encodeURIComponent(assetUrl)}`, getInit('GET'))
.then(handleResp)
return coralApi(`/stream?asset_url=${encodeURIComponent(assetUrl)}`)
.then((json) => {
/* Add items to the store */
@@ -166,8 +165,7 @@ export function getStream (assetUrl) {
export function getItemsArray (ids) {
return (dispatch) => {
return fetch(`${base}/item/${ids}`, getInit('GET'))
.then(handleResp)
return coralApi(`/item/${ids}`)
.then((json) => {
for (let i = 0; i < json.items.length; i++) {
dispatch(addItem(json.items[i]));
@@ -196,8 +194,7 @@ export function postItem (item, type, id) {
if (id) {
item.id = id;
}
return fetch(`${base}/${type}`, getInit('POST', item))
.then(handleResp)
return coralApi(`/${type}`, {method: 'POST', body: item})
.then((json) => {
dispatch(addItem({...item, id:json.id}, type));
return json.id;
@@ -227,8 +224,7 @@ export function postAction (item_id, action_type, user_id, item_type) {
user_id
};
return fetch(`${base}/${item_type}/${item_id}/actions`, getInit('POST', action))
.then(handleResp);
return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action});
};
}
@@ -249,7 +245,6 @@ export function postAction (item_id, action_type, user_id, item_type) {
export function deleteAction (action_id) {
return () => {
return fetch(`${base}/actions/${action_id}`, {method: 'DELETE'})
.then(handleResp);
return coralApi(`/actions/${action_id}`, {method: 'DELETE'});
};
}
+21
View File
@@ -0,0 +1,21 @@
import * as actions from '../constants/user';
import {addNotification} from '../actions/notification';
import coralApi from '../helpers/response';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
const saveBioRequest = () => ({type: actions.SAVE_BIO_REQUEST});
const saveBioSuccess = settings => ({type: actions.SAVE_BIO_SUCCESS, settings});
const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
export const saveBio = (user_id, formData) => dispatch => {
dispatch(saveBioRequest());
coralApi(`/user/${user_id}/bio`, {method: 'PUT', body: formData})
.then(({settings}) => {
dispatch(addNotification('success', lang.t('successBioUpdate')));
dispatch(saveBioSuccess(settings));
})
.catch(error => dispatch(saveBioFailure(error)));
};
@@ -0,0 +1,4 @@
.message {
background: #D8D8D8;
padding: 25px;
}
@@ -0,0 +1,20 @@
import React from 'react';
import styles from './RestrictedContent.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
export default ({children, restricted, message = lang.t('contentNotAvailable'), restrictedComp}) => {
if (restricted) {
return restrictedComp ? restrictedComp : messageBox(message);
} else {
return (
<div>
{children}
</div>
);
}
};
const messageBox = (message) => <div className={styles.message}>{message}</div>;
@@ -0,0 +1,8 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
export default () => (
<span>{lang.t('suspendedAccountMsg')}</span>
);
+3
View File
@@ -0,0 +1,3 @@
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
+13 -7
View File
@@ -1,23 +1,25 @@
export const base = '/api/v1';
export const getInit = (method, body) => {
let init = {
method,
const buildOptions = (inputOptions = {}) => {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'same-origin'
};
const options = Object.assign({}, defaultOptions, inputOptions);
if (method.toLowerCase() !== 'get') {
init.body = JSON.stringify(body);
if (options.method.toLowerCase() !== 'get') {
options.body = JSON.stringify(options.body);
}
return init;
return options;
};
export const handleResp = res => {
const handleResp = res => {
if (res.status === 401) {
throw new Error('Not Authorized to make this request');
} else if (res.status > 399) {
@@ -28,3 +30,7 @@ export const handleResp = res => {
return res.json();
}
};
export default (url, options) => {
return fetch(`${base}${url}`, buildOptions(options)).then(handleResp);
};
+8 -3
View File
@@ -13,6 +13,11 @@ const initialState = Map({
successSignUp: false
});
const purge = user => {
const {settings, profiles, ...userData} = user; // eslint-disable-line
return userData;
};
export default function auth (state = initialState, action) {
switch (action.type) {
case actions.SHOW_SIGNIN_DIALOG :
@@ -44,11 +49,11 @@ export default function auth (state = initialState, action) {
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('loggedIn', true)
.set('user', action.user);
.set('user', purge(action.user));
case actions.FETCH_SIGNIN_SUCCESS:
return state
.set('loggedIn', true)
.set('user', action.user);
.set('user', purge(action.user));
case actions.FETCH_SIGNIN_FAILURE:
return state
.set('isLoading', false)
@@ -56,7 +61,7 @@ export default function auth (state = initialState, action) {
.set('user', null);
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return state
.set('user', action.user)
.set('user', purge(action.user))
.set('loggedIn', true);
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return state
+2
View File
@@ -5,6 +5,7 @@ import config from './config';
import items from './items';
import notification from './notification';
import auth from './auth';
import user from './user';
/**
* Expose the combined main reducer
@@ -15,4 +16,5 @@ export default combineReducers({
items,
notification,
auth,
user
});
+36
View File
@@ -0,0 +1,36 @@
import {Map} from 'immutable';
import * as authActions from '../constants/auth';
import * as actions from '../constants/user';
const initialState = Map({
displayName: '',
profiles: [],
settings: {}
});
const purge = user => {
const {_id, created_at, updated_at, __v, roles, ...userData} = user; // eslint-disable-line
return userData;
};
export default function user (state = initialState, action) {
switch (action.type) {
case authActions.CHECK_LOGIN_SUCCESS:
return state.merge(Map(purge(action.user)));
case authActions.CHECK_LOGIN_FAILURE:
return initialState;
case authActions.FETCH_SIGNIN_SUCCESS:
return state.merge(Map(purge(action.user)));
case authActions.FETCH_SIGNIN_FAILURE:
return initialState;
case authActions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return state.merge(Map(purge(action.user)));
case authActions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return initialState;
case actions.SAVE_BIO_SUCCESS:
return state
.set('settings', action.settings);
default :
return state;
}
}
+7 -1
View File
@@ -1,5 +1,8 @@
{
"en": {
"successBioUpdate": "Your Bio has been updated",
"contentNotAvailable": "This content is not available",
"suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
"error": {
"email": "Not a valid E-Mail",
"password": "Password must be at least 8 characters",
@@ -10,6 +13,9 @@
}
},
"es": {
"successBioUpdate": "Tu bio fue actualizada",
"contentNotAvailable": "El contenido no se encuentra disponible",
"suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"error": {
"email": "No es un email válido",
"password": "La contraseña debe tener por lo menos 8 caracteres",
@@ -19,4 +25,4 @@
"emailInUse": "Email address already in use"
}
}
}
}
+40 -6
View File
@@ -1,9 +1,43 @@
import React from 'react';
import React, {Component} from 'react';
import {Tooltip} from 'coral-ui';
const packagename = 'coral-plugin-author-name';
const AuthorName = ({author}) =>
<div className={`${packagename}-text`}>
{author && author.displayName}
</div>;
export default class AuthorName extends Component {
constructor (props) {
super(props);
export default AuthorName;
this.state = {
showTooltip: false
};
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
}
handleMouseOver () {
this.setState({
showTooltip: true
});
}
handleMouseLeave () {
this.setState({
showTooltip: false
});
}
render () {
const {author} = this.props;
const {showTooltip} = this.state;
return (
<div
className={`${packagename}-text`}
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
>
{author && author.displayName}
{ showTooltip && <Tooltip>{author.settings.bio}</Tooltip>}
</div>
);
}
}
+4 -4
View File
@@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import {Button} from 'coral-ui';
const name = 'coral-plugin-commentbox';
@@ -75,12 +76,11 @@ class CommentBox extends Component {
</div>
<div className={`${name}-button-container`}>
{ author && (
<button
className={`${name}-button`}
style={styles && styles.button}
<Button
cStyle='darkGrey'
onClick={this.postComment}>
{lang.t('post')}
</button>
</Button>
)
}
</div>
+2 -1
View File
@@ -4,10 +4,11 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, addNotification, currentUser}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (!flagged) {
+2 -1
View File
@@ -4,10 +4,11 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (!liked) {
@@ -9,8 +9,8 @@ const lang = new I18n(translations);
class PermalinkButton extends React.Component {
static propTypes = {
asset_id: PropTypes.string.isRequired,
comment_id: PropTypes.string.isRequired
articleURL: PropTypes.string.isRequired,
commentId: PropTypes.string.isRequired
}
constructor (props) {
@@ -43,29 +43,27 @@ class PermalinkButton extends React.Component {
}
render () {
const publisherUrl = `${location.protocol}//${location.host}/`;
return (
<div className={`${name}-container`} style={styles}>
<div className={`${name}-container`}>
<button onClick={this.toggle} className={`${name}-button`}>
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
{lang.t('permalink.permalink')}
</button>
<div
style={styles.popover(this.state.popoverOpen)}
className={`${name}-popover`}>
className={`${name}-popover ${this.state.popoverOpen ? 'active' : ''}`}>
<input
className={`${name}-copy-field`}
type='text'
ref={input => this.permalinkInput = input}
value={`${publisherUrl}${this.props.asset_id}#${this.props.comment_id}`}
value={`${this.props.articleURL}#${this.props.commentId}`}
onChange={() => {}} />
<button className={`${name}-copy-button`} onClick={this.copyPermalink}>Copy</button>
{
this.state.copySuccessful ? <p>copied to clipboard</p> : null
this.state.copySuccessful ? <p className={`${name}-copied-text`}>copied to clipboard</p> : null
}
{
this.state.copyFailure
? <p>copying to clipboard not supported in this browser. Use Cmd + C.</p>
? <p className={`${name}-copied-error`}>copying to clipboard not supported in this browser. Use Cmd + C.</p>
: null
}
</div>
@@ -75,20 +73,3 @@ class PermalinkButton extends React.Component {
}
export default onClickOutside(PermalinkButton);
const styles = {
position: 'relative',
popover: active => {
return {
display: active ? 'block' : 'none',
backgroundColor: 'white',
border: '1px solid black',
minWidth: 400,
position: 'absolute',
top: 30,
right: 0,
padding: 5
};
}
};
+8 -6
View File
@@ -2,15 +2,17 @@ import React from 'react';
import styles from './Bio.css';
import {Button} from '../../coral-ui';
export default () => (
export default ({bio, handleSave, handleInput, handleCancel}) => (
<div className={styles.bio}>
<h1>Bio</h1>
<p>Tell the community about yourself</p>
<textarea />
<div className={styles.actions}>
<Button cStyle='cancel' raised>Cancel</Button>
<Button cStyle='success'>Save Changes</Button>
</div>
<form>
<textarea value={bio} onChange={handleInput} />
<div className={styles.actions}>
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
</div>
</form>
</div>
);
@@ -0,0 +1,15 @@
.message {
padding: 10px 0 20px;
letter-spacing: 0.1px;
font-size: 13px;
line-height: 33px;
}
.message a {
color: black;
font-weight: bold;
cursor: pointer;
margin: 0px;
padding-bottom: 2px;
border-bottom: solid 1px black;
}
@@ -0,0 +1,17 @@
import React from 'react';
import styles from './NotLoggedIn.css';
export default ({showSignInDialog}) => (
<div className={styles.message}>
<div>
<a onClick={showSignInDialog}>Sign In</a> to access Settings
</div>
<div>
From the Settings Page you can
<ul>
<li>See your comment history</li>
<li>Write a bio about yourself to display to the community</li>
</ul>
</div>
</div>
);
@@ -1,10 +1,10 @@
import React from 'react';
import styles from './SettingsHeader.css';
export default () => (
export default ({userData}) => (
<div className={styles.header}>
<h1>Jackson</h1>
<h2>jackson_persona@gmail.com</h2>
<h1>{userData.displayName}</h1>
<h2>{userData.profiles.map(profile => profile.id)}</h2>
</div>
);
@@ -0,0 +1,45 @@
import React, {Component} from 'react';
import Bio from '../components/Bio';
export default class BioContainer extends Component {
constructor (props) {
super(props);
this.state = {
bio: props.bio
};
this.handleSave = this.handleSave.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleInput(e) {
this.setState({
bio: e.target.value
});
}
handleSave (e) {
e.preventDefault();
const {userData, saveBio} = this.props;
const {bio} = this.state;
saveBio(userData.id, {bio});
}
handleCancel () {
this.setState({
bio: this.props.bio
});
}
render () {
return <Bio
bio={this.state.bio}
userData={this.props.userData}
handleSave={this.handleSave}
handleInput={this.handleInput}
handleCancel={this.handleCancel}
/>;
}
}
@@ -1,24 +1,26 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {TabBar, Tab} from '../../coral-ui';
import {saveBio} from 'coral-framework/actions/user';
import Bio from '../components/Bio';
import BioContainer from './BioContainer';
import NotLoggedIn from '../components/NotLoggedIn';
import {TabBar, Tab, TabContent} from '../../coral-ui';
import CommentHistory from '../components/CommentHistory';
import SettingsHeader from '../components/SettingsHeader';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
class SignInContainer extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0
activeTab: 0,
};
this.handleTabChange = this.handleTabChange.bind(this);
}
componentWillMount () {
// Get Bio
// Fetch commentHistory
}
@@ -29,18 +31,22 @@ class SignInContainer extends Component {
}
render() {
//const {embedStream} = this.props;
const {loggedIn, userData, showSignInDialog} = this.props;
const {activeTab} = this.state;
return (
<div>
<SettingsHeader />
<RestrictedContent restricted={!loggedIn} restrictedComp={<NotLoggedIn showSignInDialog={showSignInDialog} />}>
<SettingsHeader {...this.props} />
<TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
<Tab>All Comments (120)</Tab>
<Tab>Profile Settings</Tab>
</TabBar>
{ activeTab === 0 && <CommentHistory {...this.props}/> }
{ activeTab === 1 && <Bio {...this.props}/> }
</div>
<TabContent show={activeTab === 0}>
<CommentHistory {...this.props}/>
</TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
</TabContent>
</RestrictedContent>
);
}
}
@@ -49,10 +55,8 @@ const mapStateToProps = () => ({
});
const mapDispatchToProps = dispatch => ({
getBio: () => dispatch(),
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)),
getHistory: () => dispatch(),
handleSaveChanges: () => dispatch(),
handleCancel: () => dispatch()
});
export default connect(
@@ -44,7 +44,7 @@ class ForgotContent extends React.Component {
}
{
passwordRequestFailure
? <p className={styles.attention}>{passwordRequestFailure}</p>
? <p className={styles.passwordRequestFailure}>{passwordRequestFailure}</p>
: null
}
</form>
+5 -2
View File
@@ -1,12 +1,15 @@
import React from 'react';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const UserBox = ({className, user, logout, ...props}) => (
<div
className={`${styles.userBox} ${className}`}
className={`${styles.userBox} ${className ? className : ''}`}
{...props}
>
Signed in as <a>{user.displayName}</a>. Not you? <a onClick={logout}>Sign out</a>
{lang.t('signIn.loggedInAs')} <a>{user.displayName}</a>. {lang.t('signIn.notYou')} <a onClick={logout}>{lang.t('signIn.logout')}</a>
</div>
);
+11 -2
View File
@@ -103,10 +103,18 @@ input.error{
color: #B71C1C;
}
.userBox {
padding: 10px 0 20px;
letter-spacing: 0.1px;
}
.userBox a {
color: #2c69b6;
color: black;
font-weight: bold;
cursor: pointer;
margin: 0px;
padding-bottom: 2px;
border-bottom: solid 1px black;
}
.attention {
@@ -138,5 +146,6 @@ input.error{
.passwordRequestFailure {
border: 1px solid orange;
background-color: 1px solid coral
background-color: 1px solid coral;
padding: 10px;
}
@@ -129,12 +129,10 @@ class SignInContainer extends Component {
}
render() {
const {auth, showSignInDialog} = this.props;
const {auth, showSignInDialog, noButton} = this.props;
return (
<div>
<Button onClick={showSignInDialog} full>
Sign in to comment
</Button>
{!noButton && <Button onClick={showSignInDialog} full> Sign in to comment</Button>}
<SignDialog
open={auth.showSignInDialog}
view={auth.view}
+4
View File
@@ -1,6 +1,8 @@
export default {
en: {
'signIn': {
notYou: 'Not you?',
loggedInAs: 'Logged in as',
facebookSignIn: 'Sign in with Facebook',
facebookSignUp: 'Sign up with Facebook',
logout: 'Logout',
@@ -24,6 +26,8 @@ export default {
},
es: {
'signIn': {
notYou: 'No eres tu?',
loggedInAs: 'Entraste como',
facebookSignIn: 'Entrar con Facebook',
facebookSignUp: 'Regístrate con Facebook',
logout: 'Salir',
+10
View File
@@ -67,6 +67,16 @@
background: #4f5c67;
}
.type--darkGrey {
color: white;
background: #696969;
}
.type--darkGrey:hover {
color: white;
background: #696969;
}
.full {
width: 100%;
margin: 0;
+1
View File
@@ -15,6 +15,7 @@
padding: 8px 10px;
margin-right: -1px;
user-select: none;
font-size: 13px;
}
.base li:hover {
+36
View File
@@ -0,0 +1,36 @@
.tooltip {
display: inline-block;
position: absolute;
width: 100%;
border: solid 1px #2376D8;
top: 33px;
left: 0;
box-sizing: border-box;
background: white;
border-radius: 3px;
padding: 20px 10px;
z-index: 3;
/*box-shadow: 1px 1px 4px rgba(0,105,255,.5);*/
}
.tooltip:before{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
left: 2em;
top: -19px;
transform: rotate(180deg);
z-index: 2;
}
.tooltip:after{
content: '';
border: 10px solid transparent;
border-top-color: #2376D8;
position: absolute;
left: 2em;
top: -20px;
transform: rotate(180deg);
z-index: 1;
}
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
import styles from './Tooltip.css';
export default ({children}) => (
<span className={styles.tooltip}>{children}</span>
);
+2
View File
@@ -5,3 +5,5 @@ export {default as TabBar} from './components/TabBar';
export {default as Tab} from './components/Tab';
export {default as TabContent} from './components/TabContent';
export {default as Button} from './components/Button';
export {default as Spinner} from './components/Spinner';
export {default as Tooltip} from './components/Tooltip';
+2
View File
File diff suppressed because one or more lines are too long
+26 -1
View File
@@ -73,7 +73,15 @@ const UserSchema = new mongoose.Schema({
// Roles provides an array of roles (as strings) that is associated with a
// user.
roles: [String]
roles: [String],
// User's settings
settings: {
bio: {
type: String,
default: ''
}
}
}, {
// This will ensure that we have proper timestamps available on this model.
@@ -521,3 +529,20 @@ UserService.count = () => {
UserService.all = () => {
return UserModel.find();
};
/**
* Adds a new User bio
* @return {Promise}
*/
UserService.addBio = (id, bio) => (
UserModel.findOneAndUpdate({
id
}, {
$set: {
'settings.bio': bio
}
}, {
new: true
})
);
+1 -5
View File
@@ -1,11 +1,7 @@
const mongoose = require('mongoose');
const debug = require('debug')('talk:db');
const enabled = require('debug').enabled;
let url = process.env.TALK_MONGO_URL || 'mongodb://localhost';
if (process.env.NODE_ENV === 'test') {
url = 'mongodb://localhost/coral-test';
}
const url = process.env.TALK_MONGO_URL || (process.env.NODE_ENV === 'test' ? 'mongodb://localhost/test' : 'mongodb://localhost/talk');
// Use native promises
mongoose.Promise = global.Promise;
+22 -7
View File
@@ -1,8 +1,11 @@
const express = require('express');
const Comment = require('../../../models/comment');
const Asset = require('../../../models/asset');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const wordlist = require('../../../services/wordlist');
const authorization = require('../../../middleware/authorization');
const _ = require('lodash');
const router = express.Router();
@@ -41,13 +44,25 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
query = assetIDWrap(Comment.all());
}
query
.then(comments => {
res.json(comments);
})
.catch((err) => {
next(err);
});
query.then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]);
})
.then(([comments, users, actions])=>
res.status(200).json({
comments,
users,
actions
}))
.catch((err) => {
next(err);
});
});
router.post('/', wordlist.filter('body'), (req, res, next) => {
+18 -2
View File
@@ -1,7 +1,10 @@
const express = require('express');
const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const Setting = require('../../../models/setting');
const Asset = require('../../../models/asset');
const _ = require('lodash');
const router = express.Router();
@@ -41,8 +44,21 @@ router.get('/comments/pending', (req, res, next) => {
.then(({moderation}) => {
return Comment.moderationQueue(moderation);
}).then((comments) => {
res.json(comments);
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]);
})
.then(([comments, users, actions]) => {
res.json({
comments,
users,
actions
});
})
.catch(error => {
next(error);
+2 -2
View File
@@ -34,13 +34,13 @@ router.get('/', (req, res, next) => {
// Merge the asset specific settings with the returned settings object in
// the event that the asset that was returned also had settings.
if (asset.settings) {
settings = Object.assign(settings, asset.settings);
settings = Object.assign({}, settings, asset.settings);
}
// Fetch the appropriate comments stream.
let comments;
if (settings.moderation === 'post') {
if (settings.moderation === 'pre') {
comments = Comment.findAcceptedByAssetId(asset.id);
} else {
comments = Comment.findAcceptedAndNewByAssetId(asset.id);
+17
View File
@@ -140,4 +140,21 @@ router.post('/request-password-reset', (req, res, next) => {
});
});
router.put('/:user_id/bio', (req, res, next) => {
const {user_id} = req.params;
const {bio} = req.body;
if (!bio) {
return next('You must submit a new bio');
}
User
.addBio(user_id, bio)
.then(user => res.status(200).send(user))
.catch(error => {
const errorMsg = typeof error === 'string' ? error : error.message;
res.status(500).json({error: errorMsg});
});
});
module.exports = router;
+8 -2
View File
@@ -6,11 +6,17 @@ router.use('/admin', require('./admin'));
router.use('/embed', require('./embed'));
router.get('/', (req, res) => {
res.render('article', {title: 'Coral Talk'});
return res.render('article', {
title: 'Coral Talk',
basePath: '/client/embed/stream'
});
});
router.get('/assets/:asset_title', (req, res) => {
res.render('article', {title: req.params.asset_title.split('-').join(' ')});
return res.render('article', {
title: req.params.asset_title.split('-').join(' '),
basePath: '/client/embed/stream'
});
});
module.exports = router;
+16 -13
View File
@@ -19,7 +19,7 @@ const settings = {id: '1', moderation: 'pre'};
describe('/api/v1/comments', () => {
describe('#get', () => {
describe.only('#get', () => {
const comments = [{
body: 'comment 10',
asset_id: 'asset',
@@ -100,8 +100,9 @@ describe('/api/v1/comments', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.length(1);
expect(res.body[0]).to.have.property('id', comments[2].id);
expect(res.body).to.have.property('comments');
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', comments[2].id);
});
});
@@ -111,8 +112,8 @@ describe('/api/v1/comments', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.length(1);
expect(res.body[0]).to.have.property('id', comments[3].id);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', comments[3].id);
});
});
@@ -122,7 +123,7 @@ describe('/api/v1/comments', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.length(2);
expect(res.body.comments).to.have.length(2);
});
});
@@ -133,8 +134,8 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.length(1);
expect(res.body[0]).to.have.property('id', comments[0].id);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', comments[0].id);
});
});
});
@@ -300,18 +301,22 @@ describe('/api/v1/comments/:comment_id/actions', () => {
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: ''
status: []
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
status: [{
type: 'rejected'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted'
status: [{
type: 'accepted'
}]
}];
const users = [{
@@ -349,10 +354,8 @@ describe('/api/v1/comments/:comment_id/actions', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('item_type', 'comment');
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('item_id', 'abc');
expect(res.body).to.have.property('user_id', '456');
});
});
});
+29 -18
View File
@@ -62,25 +62,36 @@ describe('/api/v1/queue', () => {
}];
beforeEach(() => {
return Promise.all([
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions),
Setting.init(settings)
]);
return User.createLocalUsers(users)
.then((u) => {
comments[0].author_id = u[0].id;
comments[1].author_id = u[1].id;
comments[2].author_id = u[1].id;
return Comment.create(comments);
})
.then((c) => {
actions[0].item_id = c[0].id;
actions[1].item_id = c[1].id;
return Promise.all([
Action.create(actions),
Setting.init(settings)
]);
});
});
describe('#get', () => {
it('should return all the pending comments', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('id', 'def');
done();
});
});
it('should return all the pending comments, users and actions', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('body');
expect(res.body.users[0]).to.have.property('displayName');
expect(res.body.actions[0]).to.have.property('action_type');
done();
});
});
});
+89 -91
View File
@@ -14,96 +14,94 @@ const Asset = require('../../../../models/asset');
const Setting = require('../../../../models/setting');
describe('/api/v1/stream', () => {
const settings = {
id: '1',
moderation: 'pre'
};
const comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: []
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: [{
type: 'rejected'
}]
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Promise.all([
User.createLocalUsers(users),
Asset.findOrCreateByUrl('http://test.com'),
Asset
.findOrCreateByUrl('http://coralproject.net/asset2')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {moderation: 'post'})
.then(() => asset);
})
])
.then(([users, asset1, asset2]) => {
comments[0].author_id = users[0].id;
comments[1].author_id = users[1].id;
comments[2].author_id = users[0].id;
comments[3].author_id = users[1].id;
comments[0].asset_id = asset1.id;
comments[1].asset_id = asset1.id;
comments[2].asset_id = asset2.id;
comments[3].asset_id = asset2.id;
return Promise.all([
Comment.create(comments),
Action.create(actions),
Setting.init(settings)
]);
});
});
describe('#get', () => {
const settings = {
id: '1',
moderation: 'post'
};
const comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: []
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: [{
type: 'rejected'
}]
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Promise.all([
User.createLocalUsers(users),
Asset.findOrCreateByUrl('http://test.com'),
Asset
.findOrCreateByUrl('http://coralproject.net/asset2')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {moderation: 'pre'})
.then(() => asset);
})
])
.then(([users, asset1, asset2]) => {
comments[0].author_id = users[0].id;
comments[1].author_id = users[1].id;
comments[2].author_id = users[0].id;
comments[3].author_id = users[1].id;
comments[0].asset_id = asset1.id;
comments[1].asset_id = asset1.id;
comments[2].asset_id = asset2.id;
comments[3].asset_id = asset2.id;
return Promise.all([
Comment.create(comments),
Action.create(actions),
Setting.init(settings)
]);
});
});
it('should return a stream with comments, users and actions for an existing asset', () => {
return chai.request(app)
.get('/api/v1/stream')
@@ -114,7 +112,7 @@ describe('/api/v1/stream', () => {
expect(res.body.comments.length).to.equal(2);
expect(res.body.users.length).to.equal(2);
expect(res.body.actions.length).to.equal(1);
expect(res.body.settings).to.have.property('moderation', 'pre');
expect(res.body.settings).to.have.property('moderation', 'post');
});
});
@@ -127,7 +125,7 @@ describe('/api/v1/stream', () => {
expect(res.body.assets.length).to.equal(1);
expect(res.body.comments.length).to.equal(1);
expect(res.body.users.length).to.equal(1);
expect(res.body.settings).to.have.property('moderation', 'post');
expect(res.body.settings).to.have.property('moderation', 'pre');
});
});
});
+20 -8
View File
@@ -32,13 +32,25 @@
<div id='coralStreamEmbed'></div>
</main>
<!--- Script --->
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
<script>
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
pymParent.onMessage('height', function(height) {
document.querySelector('#coralStreamEmbed iframe').height = height + 'px';
});
</script>
<script type='text/javascript' src='<%= basePath %>/pym.v1.min.js'></script>
<script>
var ready = false;
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})
pymParent.onMessage('childReady', function () {
var interval = setInterval(function () {
if (ready) {
window.clearInterval(interval);
pymParent.sendMessage('DOMContentLoaded', window.location.hash);
}
}, 100);
});
// wait till images and other iframes are loaded before scrolling the page.
// or do we want to be more aggressive and scroll when we hit DOM ready?
document.addEventListener('DOMContentLoaded', function () {
ready = true;
});
</script>
</body>
</html>
+19 -7
View File
@@ -19,14 +19,26 @@
</head>
<body>
<div id='coralStreamEmbed'></div>
<!--- Script --->
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
<script type='text/javascript' src='<%= basePath %>/pym.v1.min.js'></script>
<script>
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
pymParent.onMessage('height', function(height) {
document.querySelector('#coralStreamEmbed iframe').height = height + 'px';
});
var ready = false;
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'});
pymParent.onMessage('childReady', function () {
var interval = setInterval(function () {
if (ready) {
window.clearInterval(interval);
pymParent.sendMessage('DOMContentLoaded', window.location.hash);
}
}, 100);
});
// wait till images and other iframes are loaded before scrolling the page.
// or do we want to be more aggressive and scroll when we hit DOM ready?
document.addEventListener('DOMContentLoaded', function (e) {
ready = true;
});
</script>
</body>
</html>
+10 -4
View File
@@ -74,10 +74,16 @@ module.exports = {
]
},
plugins: [
new Copy(buildEmbeds.map(embed => ({
from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'),
to: path.join(__dirname, 'dist', 'embed', embed)
}))),
new Copy([
...buildEmbeds.map(embed => ({
from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'),
to: path.join(__dirname, 'dist', 'embed', embed)
})),
{
from: path.join(__dirname, 'client', 'lib'),
to: path.join(__dirname, 'dist', 'embed', 'stream')
}
]),
autoprefixer,
precss,
new webpack.ProvidePlugin({