diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9fa7985c0 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 7cba85788..3481ad671 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -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 (
  • person - {props.comment.get('name') || lang.t('comment.anon')} - {timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - {props.comment.get('flagged') ?

    {lang.t('comment.flagged')}

    : null} + {author.get('displayName') || lang.t('comment.anon')} + {timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} + {comment.get('flagged') ?

    {lang.t('comment.flagged')}

    : null}
    {links ? Contains Link : null}
    - {props.actions.map((action, i) => canShowAction(action, props.comment) ? ( + {props.actions.map((action, i) => canShowAction(action, comment) ? ( props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))} + onClick={() => props.onClickAction(props.actionsMap[action].status, comment.get('id'))} /> ) : null)}
    @@ -40,7 +41,7 @@ export default props => {
    - {props.comment.get('body')} + {comment.get('body')}
    diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index e4682252d..40b99b892 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -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 (
      - {commentIds.map((commentId, index) => ( - { + const comment = comments.get(commentId); + return { 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()}
    ); } diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.js b/client/coral-admin/src/containers/CommentStream/CommentStream.js index b1e002549..113a7bc86 100644 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.js +++ b/client/coral-admin/src/containers/CommentStream/CommentStream.js @@ -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 (
    @@ -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); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index a4d82fddc..e5670d169 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -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); diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js index f897c1bae..095ef7aac 100644 --- a/client/coral-admin/src/reducers/auth.js +++ b/client/coral-admin/src/reducers/auth.js @@ -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; } diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 61029539a..1f1b444fc 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -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 }); - diff --git a/client/coral-admin/src/reducers/users.js b/client/coral-admin/src/reducers/users.js new file mode 100644 index 000000000..872ae904a --- /dev/null +++ b/client/coral-admin/src/reducers/users.js @@ -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())); +}; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 15c9cf76a..5502df7ed 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -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; @@ -38,12 +35,31 @@ Promise.all([ coralApi('/comments?action_type=flag') ]) .then(([pending, rejected, flagged]) => { - flagged.forEach(comment => comment.flagged = true); - return [...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 diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index d2a7ddd18..7973c2a85 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -1,4 +1,7 @@ import React, {Component, PropTypes} from 'react'; +import Pym from 'pym.js'; +import {connect} from 'react-redux'; + import { itemActions, Notification, @@ -6,7 +9,7 @@ import { authActions, configActions } 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'; @@ -14,47 +17,22 @@ 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, Button} from '../../coral-ui'; + +import {TabBar, Tab, TabContent, Spinner, Button} from '../../coral-ui'; import SettingsContainer from '../../coral-settings/containers/SettingsContainer'; -import {Icon} from 'react-mdl'; +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 {logout, showSignInDialog} = authActions; const {updateOpenStatus} = configActions; -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()), - updateStatus: status => dispatch(updateOpenStatus(status)) -}); - class CommentStream extends Component { constructor (props) { @@ -87,47 +65,22 @@ class CommentStream extends Component { componentDidMount () { // Set up messaging between embedded Iframe an parent component // Using recommended Pym init code which violates .eslint standards - this.pym = new Pym.Child({polling: 100}); + const pym = new Pym.Child({polling: 100}); - const path = this.pym.parentUrl.split('#')[0]; - - this.props.getStream(path || window.location); - this.path = path; - - this.pym.sendMessage('childReady'); - - this.pym.onMessage('DOMContentLoaded', hash => { - // the comment ids can start with numbers, which is invalid for DOM id attributes - 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); - }); + if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) { + this.props.getStream(pym.parentUrl); + } else { + this.props.getStream(window.location); + } } 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 @@ -142,73 +95,76 @@ class CommentStream extends Component { return
    { rootItem - ?
    + ?
    Settings Configure Stream - - - { - status === 'open' - ?
    - - {loggedIn && } - - {!loggedIn && } -
    - :

    Comments are closed for this thread

    - } - { - rootItem.comments && rootItem.comments.map((commentId) => { - const comment = comments[commentId]; - return
    -
    - - - -
    - - } + {/* Add to the restricted param a boolean if the user is suspended*/} + }> + + { + status === 'open' + ?
    + + + id={rootItemId} + premod={this.props.config.moderation} + reply={false} + author={user} + /> + {!loggedIn && }
    -
    - + :

    Comments are closed for this thread.

    + } + { + rootItem.comments && rootItem.comments.map((commentId) => { + const comment = comments[commentId]; + return
    +
    + + + +
    + + +
    +
    + -
    +
    - - -
    -
    - - -
    - + +
    +
    + + +
    +
    ; }) } @@ -282,15 +240,26 @@ class CommentStream extends Component { /> - +

    {status === 'open' ? 'Close' : 'Open'} Comment Stream

    + +
    - : 'Loading' + : + }
    ; } @@ -315,4 +284,28 @@ const CloseCommentsInfo = ({ status, onClick }) => status === 'open' ? (
    ) +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()), + updateStatus: status => dispatch(updateOpenStatus(status)) +}); + export default connect(mapStateToProps, mapDispatchToProps)(CommentStream); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index f83541527..59efc3dc8 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -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; } @@ -84,6 +86,7 @@ hr { .coral-plugin-commentbox-textarea { flex: 1; padding: 5px; + min-height: 100px; } .coral-plugin-commentbox-button-container { @@ -110,6 +113,7 @@ hr { /* Comment styles */ .comment { + position: relative; margin-bottom: 10px; position: relative; } diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js new file mode 100644 index 000000000..72075e6db --- /dev/null +++ b/client/coral-framework/actions/user.js @@ -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))); +}; diff --git a/client/coral-framework/components/RestrictedContent.css b/client/coral-framework/components/RestrictedContent.css new file mode 100644 index 000000000..47ced7b0b --- /dev/null +++ b/client/coral-framework/components/RestrictedContent.css @@ -0,0 +1,4 @@ +.message { + background: #D8D8D8; + padding: 25px; +} diff --git a/client/coral-framework/components/RestrictedContent.js b/client/coral-framework/components/RestrictedContent.js new file mode 100644 index 000000000..32aaf632d --- /dev/null +++ b/client/coral-framework/components/RestrictedContent.js @@ -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 ( +
    + {children} +
    + ); + } +}; + +const messageBox = (message) =>
    {message}
    ; diff --git a/client/coral-framework/components/SuspendedAccount.js b/client/coral-framework/components/SuspendedAccount.js new file mode 100644 index 000000000..5a23b77a2 --- /dev/null +++ b/client/coral-framework/components/SuspendedAccount.js @@ -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 () => ( + {lang.t('suspendedAccountMsg')} +); diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js new file mode 100644 index 000000000..0c316d48a --- /dev/null +++ b/client/coral-framework/constants/user.js @@ -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'; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index a445fc663..a4803fa96 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -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 diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js index 90eaa7cb7..a2f439028 100644 --- a/client/coral-framework/reducers/index.js +++ b/client/coral-framework/reducers/index.js @@ -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 }); diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js new file mode 100644 index 000000000..11b57fc15 --- /dev/null +++ b/client/coral-framework/reducers/user.js @@ -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; + } +} diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index 4abd1ed03..ab65ba565 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -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" } } -} \ No newline at end of file +} diff --git a/client/coral-plugin-author-name/AuthorName.js b/client/coral-plugin-author-name/AuthorName.js index aef819468..56b868726 100644 --- a/client/coral-plugin-author-name/AuthorName.js +++ b/client/coral-plugin-author-name/AuthorName.js @@ -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}) => -
    - {author && author.displayName} -
    ; +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 ( +
    + {author && author.displayName} + { showTooltip && {author.settings.bio}} +
    + ); + } +} diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 79641d856..9ae524652 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -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 {
    { author && ( - + ) }
    diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index cbb0fef63..f0ab1b22f 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -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) { diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 07f27c426..b3d31fbf4 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -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) { diff --git a/client/coral-settings/components/Bio.js b/client/coral-settings/components/Bio.js index b82587322..cd1347781 100644 --- a/client/coral-settings/components/Bio.js +++ b/client/coral-settings/components/Bio.js @@ -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}) => (

    Bio

    Tell the community about yourself

    -