From f18191793be5adfa9b98426433139fe31c3abbd8 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Sun, 23 Apr 2017 13:09:52 -0300 Subject: [PATCH 001/132] fe plugins --- docs/frontend/PLUGINS.md | 208 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/frontend/PLUGINS.md diff --git a/docs/frontend/PLUGINS.md b/docs/frontend/PLUGINS.md new file mode 100644 index 000000000..94c7b49e9 --- /dev/null +++ b/docs/frontend/PLUGINS.md @@ -0,0 +1,208 @@ +# Plugins +We can build plugins to extend the functionality of Talk. Our plugins are powered by *React*, *Redux* and *GraphQL*. We can also build them with simple vanilla javascript. +The plugins live in the `/plugins` folder. Each plugin must have an `index.js` file and two folders `client` and `server`. + +Our plugin folder structure should look like this: +``` +my-plugin/ + ├── client/ + ├── server/ + └── index.js +``` + + +### The Client Folder +The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin. + +``` +my-plugin/ + ├── client/ + │ └── index.js + ├── server/ + └── index.js +``` + +For now our `index.js` file should look like this: + +```js +export default { + // We will add more here later. +}; +``` + + +### Components +We can add our components within the `client` folder. + +``` +my-plugin/ + ├── client/ + │ ├── MyComponent.js + │ └── index.js + ├── server/ + └── index.js +``` + +#### Creating a Component +Our component could look like this: + +```js +import React, {Component} from 'react'; + +class MyButton extends Component { + render() { + return ; + } +} + +export default MyButton; +```` + +We are just creating a component that creates a `button`. Now that we created our component we need to specify where it should get injected within Talk! +To tell Talk where that Component should get injected we need to specify our *Slots*. + +Also, our Component can be a Stateless Component. + +```js +import React from 'react'; +export default = () => ; +```` + +### Slots +In Talk we have defined specific *Slots* where we can inject components. + +Here is how we specify our slots config in `my-plugin/index.js` + +```js +import MyButton from './MyButton'; + +export default { + slots: { + commentDetail: [MyButton] + } +}; +``` + +Here I’m specifying that the MyComponent Component will take place within the `commentDetail` in Talk. + +`commentDetail` it’s a specific slot in the CommentStream. It means that it will be embedded inside de comment detail. + +Slots properties take an`Array` so we can add as many components as we want. + +#### Reducers and Actions : Redux + +Talk is powered by Redux and our plugins can too! Our plugins can have their own reducers and actions. + +```js +import MyButton from './MyButton'; +import reducer from './reducer'; + +export default { + slots: { + commentDetail: [MyButton], + }, + reducer +}; +``` + +### Import Actions from Talk +We can easily trigger `Talk` actions in our plugin Components. + +```js +import React, {Component} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {addTag, removeTag} from 'coral-plugin-commentbox/actions'; + +class MyButton extends Component { + render() { + return ; + } +} + +const mapStateToProps = ({commentBox}) => ({commentBox}); + +const mapDispatchToProps = dispatch => + bindActionCreators({addTag, removeTag}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox); +``` + +### Styling our Plugin +Talk uses CSS Modules. This basically means that you can also add your CSS Module to your plugin without colliding with the rest of Talk! + +##### My Component +```js +import styles from './style.css'; + +class MyCoralButton extends Component { + render() { + return ; + } +} +```` + +Our `style.css` should could look like this. +```css + +.button { + background: coral; + border-radius: 3px; +} +``` + +## ESlint and Babel +In talk we use `eslint:recommended` and Babel with the latest ECMAScript Features. But you can use your own! +While building your plugin you need to specify a `.eslintrc.json` file and a`.babelrc` file. + +#### `.eslintrc.json` +```json +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} +```` + + +#### `. babelrc ` +```json +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} +```` + +### The server folder and the index file +Read more about the `/server` and how to extend Talk here. +[talk/PLUGINS.md at master · coralproject/talk · GitHub](https://github.com/coralproject/talk/blob/master/PLUGINS.md) + + From add7bfc17036e41b5e54f4f6e3b78dbc85065905 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Wed, 26 Apr 2017 10:33:24 -0500 Subject: [PATCH 002/132] Remove commented out css from TopRightMenu.css --- client/coral-embed-stream/src/TopRightMenu.css | 1 - 1 file changed, 1 deletion(-) diff --git a/client/coral-embed-stream/src/TopRightMenu.css b/client/coral-embed-stream/src/TopRightMenu.css index 5cddae125..0ce8a119b 100644 --- a/client/coral-embed-stream/src/TopRightMenu.css +++ b/client/coral-embed-stream/src/TopRightMenu.css @@ -20,5 +20,4 @@ position: relative; transform: rotate(180deg); top: 0; - /*top: -0.25em;*/ } From 84016e362db18f471d79d92de2ab0d2e4e1ddb2b Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Wed, 26 Apr 2017 12:37:33 -0500 Subject: [PATCH 003/132] Delete Comment UI --- client/coral-embed-stream/src/Comment.css | 47 ++++++++++- client/coral-embed-stream/src/Comment.js | 83 +++++++++++++++++-- .../src/IgnoreUserWizard.css | 14 ---- .../src/IgnoreUserWizard.js | 4 +- 4 files changed, 121 insertions(+), 27 deletions(-) delete mode 100644 client/coral-embed-stream/src/IgnoreUserWizard.css diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 8d7c7ebf9..5fe5401fe 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -13,13 +13,54 @@ pointer-events: none; } -.topRightMenu { +/* element in the top right of the Comment */ +.topRight { float: right; + margin-top: 10px; text-align: right; +} + +.topRight > * { + text-align: initial; +} + +.topRight .popover { + margin-top: 1em; + right: 0px; +} + +.topRight .popoverMenuOpen .link { + padding-bottom: 0.125em; + border-bottom: 2px solid currentColor; +} + +.topRightMenu { cursor: pointer; margin-top: 5px; } -.topRightMenu > * { - text-align: initial; +.link { + color: #2376D8; + cursor: pointer; +} + +.popover { + position: absolute; + z-index: 1; +} + +/* Wizard used for Ignore User, Delete Comment confirmations */ +.Wizard { + background-color: #2E343B; + color: white; + padding: 1em; + max-width: 220px; /* consider moving to better class */ +} + +.Wizard header { + font-weight: bold; +} + +.Wizard .textAlignRight { + text-align: right; } diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 63b3ef8dc..b5a7dfeb3 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -23,6 +23,8 @@ import Slot from 'coral-framework/components/Slot'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import {TopRightMenu} from './TopRightMenu'; import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils'; +import {Button} from 'coral-ui'; +import classnames from 'classnames'; import styles from './Comment.css'; @@ -161,6 +163,55 @@ class Comment extends React.Component { tag: BEST_TAG, }), () => 'Failed to remove best comment tag'); + class PopoverMenu extends React.Component { + static propTypes = { + children: PropTypes.node, + Popover: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + openClassName: PropTypes.string, + } + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.close = this.close.bind(this); + this.state = { + isOpen: false + }; + } + toggle() { + this.setState({isOpen: ! this.state.isOpen}); + } + close() { + this.setState({isOpen: false}); + } + render() { + const {isOpen} = this.state; + const {children, Popover, openClassName} = this.props; + return ( + + + { children } + + + { isOpen ? : null } + + + ); + } + } + + const DeleteCommentConfirmation = ({cancel, deleteComment}) => { + return ( +
+
Delete a comment
+

Are you sure you want to delete that comment

+
+ + +
+
+ ); + }; + return (
- { (currentUser && (comment.user.id !== currentUser.id)) - ? - - - : null + { (currentUser && + (comment.user.id === currentUser.id)) + + /* User can edit/delete their own comment for a short window after posting */ + ? + + { /*console.log('delete comment', comment)*/ }} + /> }> + Delete + + + + /* TopRightMenu allows currentUser to ignore other users' comments */ + : + + } diff --git a/client/coral-embed-stream/src/IgnoreUserWizard.css b/client/coral-embed-stream/src/IgnoreUserWizard.css deleted file mode 100644 index 838f2f76a..000000000 --- a/client/coral-embed-stream/src/IgnoreUserWizard.css +++ /dev/null @@ -1,14 +0,0 @@ -.IgnoreUserWizard { - background-color: #2E343B; - color: white; - padding: 1em; - max-width: 220px; -} - -.IgnoreUserWizard header { - font-weight: bold; -} - -.IgnoreUserWizard .textAlignRight { - text-align: right; -} diff --git a/client/coral-embed-stream/src/IgnoreUserWizard.js b/client/coral-embed-stream/src/IgnoreUserWizard.js index 371e6d48b..7e72bb924 100644 --- a/client/coral-embed-stream/src/IgnoreUserWizard.js +++ b/client/coral-embed-stream/src/IgnoreUserWizard.js @@ -1,5 +1,5 @@ import React, {PropTypes} from 'react'; -import styles from './IgnoreUserWizard.css'; +import styles from './Comment.css'; import {Button} from 'coral-ui'; // Guides the user through ignoring another user, including confirming their decision @@ -58,7 +58,7 @@ export class IgnoreUserWizard extends React.Component { const {step} = this.state; const elForThisStep = elsForStep[step - 1]; return ( -
+
{ elForThisStep }
); From 00a2a65d3160a1656e9a96a14f006c65d9a7254d Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 1 May 2017 16:58:24 -0500 Subject: [PATCH 004/132] Edit Comment UI --- client/coral-embed-stream/src/Comment.css | 3 +- client/coral-embed-stream/src/Comment.js | 91 +++----- .../src/EditableCommentContent.js | 62 +++++ client/coral-plugin-commentbox/CommentBox.js | 218 +++++++++++++----- 4 files changed, 254 insertions(+), 120 deletions(-) create mode 100644 client/coral-embed-stream/src/EditableCommentContent.js diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 5fe5401fe..24124f9ae 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -29,7 +29,8 @@ right: 0px; } -.topRight .popoverMenuOpen .link { +.topRight .link.active, +.topRight .active .link { padding-bottom: 0.125em; border-bottom: 2px solid currentColor; } diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index b5a7dfeb3..442c79bc1 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -23,8 +23,8 @@ import Slot from 'coral-framework/components/Slot'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import {TopRightMenu} from './TopRightMenu'; import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils'; -import {Button} from 'coral-ui'; import classnames from 'classnames'; +import {EditableCommentContent} from './EditableCommentContent'; import styles from './Comment.css'; @@ -39,7 +39,13 @@ class Comment extends React.Component { constructor(props) { super(props); - this.state = {replyBoxVisible: false}; + this.onClickEdit = this.onClickEdit.bind(this); + this.state = { + + // Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment) + isEditing: false, + replyBoxVisible: false, + }; } static propTypes = { @@ -102,6 +108,11 @@ class Comment extends React.Component { ignoreUser: React.PropTypes.func, } + onClickEdit (e) { + e.preventDefault(); + this.setState({isEditing: true}); + } + render () { const { comment, @@ -163,55 +174,6 @@ class Comment extends React.Component { tag: BEST_TAG, }), () => 'Failed to remove best comment tag'); - class PopoverMenu extends React.Component { - static propTypes = { - children: PropTypes.node, - Popover: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), - openClassName: PropTypes.string, - } - constructor(props) { - super(props); - this.toggle = this.toggle.bind(this); - this.close = this.close.bind(this); - this.state = { - isOpen: false - }; - } - toggle() { - this.setState({isOpen: ! this.state.isOpen}); - } - close() { - this.setState({isOpen: false}); - } - render() { - const {isOpen} = this.state; - const {children, Popover, openClassName} = this.props; - return ( - - - { children } - - - { isOpen ? : null } - - - ); - } - } - - const DeleteCommentConfirmation = ({cancel, deleteComment}) => { - return ( -
-
Delete a comment
-

Are you sure you want to delete that comment

-
- - -
-
- ); - }; - return (
- - { /*console.log('delete comment', comment)*/ }} - /> }> - Delete - + Edit /* TopRightMenu allows currentUser to ignore other users' comments */ @@ -257,7 +212,19 @@ class Comment extends React.Component { } - + { + this.state.isEditing + ? + : + } +
{/* TODO implmement iPerformedThisAction for the like */} diff --git a/client/coral-embed-stream/src/EditableCommentContent.js b/client/coral-embed-stream/src/EditableCommentContent.js new file mode 100644 index 000000000..09c5fae9b --- /dev/null +++ b/client/coral-embed-stream/src/EditableCommentContent.js @@ -0,0 +1,62 @@ +import React, {PropTypes} from 'react'; +import {CommentForm} from 'coral-plugin-commentbox/CommentBox'; + +/** + * Renders a Comment's body in such a way that the end-user can edit it and save changes + */ +export class EditableCommentContent extends React.Component { + + // @TODO (bengo) make sure these are accurate wrt isRequired + static propTypes = { + + // show notification to the user (e.g. for errors) + addNotification: PropTypes.func.isRequired, + asset: PropTypes.shape({ + id: PropTypes.string.isRequired, + settings: PropTypes.shape({ + charCountEnable: PropTypes.bool, + }), + }).isRequired, + + // comment that is being edited + comment: PropTypes.shape({ + body: PropTypes.string + }).isRequired, + + // logged in user + currentUser: PropTypes.shape({ + id: PropTypes.string.isRequired + }), + maxCharCount: PropTypes.number, + + parentId: PropTypes.string, + } + constructor(props) { + super(props); + } + render() { + const saveComment = function () { + }; + const originalBody = this.props.comment.body; + return ( +
+ { + + // should be disabled if user hasn't actually changed their + // original comment + return comment.body !== originalBody; + }} + saveComment={saveComment} + bodyLabel={'Edit this comment' /* @TODO (bengo) i18n */} + bodyPlaceholder="" + submitText={'Save changes' /* @TODO (bengo) i18n */} + saveButtonCStyle="green" + /> +
+ ); + } +} diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 261c95414..ee3b9722c 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -7,6 +7,132 @@ import {connect} from 'react-redux'; const name = 'coral-plugin-commentbox'; +/** + * Common UI for Creating or Editing a Comment + */ +export class CommentForm extends Component { + static propTypes = { + + // Initial value for underlying comment body textarea + defaultValue: PropTypes.string, + charCountEnable: PropTypes.bool.isRequired, + maxCharCount: PropTypes.number, + cancelButtonClicked: PropTypes.func, + + // Save the comment in the form. + // Will be passed { body: String } + saveComment: PropTypes.func.isRequired, + + // DOM ID for form input that edits comment body + bodyInputId: PropTypes.string, + + // screen reader label for input that edits comment body + bodyLabel: PropTypes.string, + + // Placeholder for input that edits comment body + bodyPlaceholder: PropTypes.string, + + // render at start of button container (useful for extra buttons) + buttonContainerStart: PropTypes.node, + + // render inside submit button + submitText: PropTypes.node, + + styles: PropTypes.shape({ + textarea: PropTypes.string + }), + + // cStyle for enabled save + saveButtonCStyle: PropTypes.string, + + // return whether the save button should be enabled for the provided + // comment ({ body }) (for reasons other than charCount) + saveCommentEnabled: PropTypes.func, + } + static get defaultProps() { + return { + bodyLabel: lang.t('comment'), + bodyPlaceholder: lang.t('comment'), + submitText: lang.t('post'), + saveButtonCStyle: 'darkGrey', + saveCommentEnabled: () => true, + }; + } + constructor(props) { + super(props); + this.onBodyChange = this.onBodyChange.bind(this); + this.onClickSubmit = this.onClickSubmit.bind(this); + this.state = { + body: props.defaultValue || '' + }; + } + onBodyChange(e) { + this.setState({body: e.target.value}); + } + onClickSubmit(e) { + e.preventDefault(); + const {saveComment} = this.props; + const {body} = this.state; + saveComment({body}); + } + render() { + const {maxCharCount, styles, saveCommentEnabled} = this.props; + + const body = this.state.body; + const length = body.length; + const isNotValidLength = (length) => !length || (maxCharCount && length > maxCharCount); + const disablePostComment = isNotValidLength(length) || ! saveCommentEnabled({body}); + + return
+
+ +