diff --git a/.babelrc b/.babelrc index 92a64c2a4..ed21974ac 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "presets": [ - ["es2015", {modules: false}] + ["es2015", {"modules": false}] ], "plugins": [ "transform-class-properties", diff --git a/.gitignore b/.gitignore index 5bf0d6527..a660453ea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ npm-debug.log* dump.rdb client/coral-framework/graphql/introspection.json +docs/source/_data/introspection.json .env *.cfg @@ -30,6 +31,7 @@ plugins/* !plugins/talk-plugin-author-menu !plugins/talk-plugin-comment-content !plugins/talk-plugin-deep-reply-count +!plugins/talk-plugin-downvote !plugins/talk-plugin-facebook-auth !plugins/talk-plugin-featured-comments !plugins/talk-plugin-flag-details @@ -52,17 +54,19 @@ plugins/* !plugins/talk-plugin-remember-sort !plugins/talk-plugin-respect !plugins/talk-plugin-slack-notifications +!plugins/talk-plugin-sort-most-downvoted !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-replied !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-sort-most-upvoted !plugins/talk-plugin-sort-newest !plugins/talk-plugin-sort-oldest !plugins/talk-plugin-subscriber !plugins/talk-plugin-toxic-comments +!plugins/talk-plugin-upvote !plugins/talk-plugin-viewing-options !plugins/talk-plugin-rich-text -!plugins/talk-plugin-rich-text-pell !plugins/talk-plugin-auth-checkbox **/node_modules/* diff --git a/.nodemon.json b/.nodemon.json index 101104f4a..5e192d80c 100644 --- a/.nodemon.json +++ b/.nodemon.json @@ -1,6 +1,6 @@ { "exec": "npm-run-all --parallel generate-introspection start:development", - "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"], + "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client", "docs/*"], "ext": "js,json,graphql,yml", "watch": [ ".", diff --git a/README.md b/README.md index 31e285607..6bdb7fda0 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,19 @@ From getting up and running, to advanced configuration, to how to scale Talk, ou ## Product Guide -Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https:/docs.coralproject.net/talk/how-talk-works). +Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://docs.coralproject.net/talk/how-talk-works). -## Relevant Links +## Pre-Launch Guide +You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/). + +## More Resources + +- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625) - [Our Blog](https://blog.coralproject.net/) - [Community Forums](https://community.coralproject.net/) - [Community Guides for Journalism](https://guides.coralproject.net/) - [More About Us](https://coralproject.net/) -- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625) ## End-to-End Testing diff --git a/client/coral-admin/src/components/CommentFormatter.js b/client/coral-admin/src/components/CommentFormatter.js deleted file mode 100644 index 8874d123f..000000000 --- a/client/coral-admin/src/components/CommentFormatter.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { matchLinks } from '../utils'; -import memoize from 'lodash/memoize'; - -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -// generate a regulare expression that catches the `phrases`. -function generateRegExp(phrases) { - const inner = phrases - .map(phrase => - phrase - .split(/\s+/) - .map(word => escapeRegExp(word)) - .join('[\\s"?!.]+') - ) - .join('|'); - - const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`; - try { - return new RegExp(pattern, 'iu'); - } catch (_err) { - // IE does not support unicode support, so we'll create one without. - return new RegExp(pattern, 'i'); - } -} - -// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases. -function getPhrasesRegexp(suspectWords, bannedWords) { - return generateRegExp([...suspectWords, ...bannedWords]); -} - -// Memoized version as arguments rarely change. -const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp); - -// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning -// an array of React Elements. -function markPhrases(body, suspectWords, bannedWords, keyPrefix) { - const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords); - const tokens = body.split(regexp); - return tokens.map( - (token, i) => - i % 3 === 2 ? {token} : token - ); -} - -// markLinks looks for links inside `body` and highlights them by returning -// an array of React Elements. -function markLinks(body) { - const matches = matchLinks(body); - const content = []; - let index = 0; - if (matches) { - matches.forEach((match, i) => { - content.push(body.substring(index, match.index)); - content.push({match.text}); - index = match.lastIndex; - }); - } - content.push(body.substring(index)); - return content; -} - -const CommentFormatter = ({ - body, - suspectWords, - bannedWords, - className = 'comment', - ...rest -}) => { - // Breaking the body by line break - const textbreaks = body.split('\n'); - - return ( - - {textbreaks.map((line, i) => { - const content = markLinks(line).map((element, index) => { - // Keep highlighted links. - if (typeof element !== 'string') { - return element; - } - - // Highlight suspect and banned phrase inside this part of text. - return markPhrases(element, suspectWords, bannedWords, index); - }); - - return ( - - {content} - {i !== textbreaks.length - 1 && ( -
- )} -
- ); - })} -
- ); -}; - -CommentFormatter.propTypes = { - className: PropTypes.string, - bannedWords: PropTypes.array, - suspectWords: PropTypes.array, - body: PropTypes.string, -}; - -export default CommentFormatter; diff --git a/client/coral-admin/src/components/Forbidden.css b/client/coral-admin/src/components/Forbidden.css new file mode 100644 index 000000000..8f93cf869 --- /dev/null +++ b/client/coral-admin/src/components/Forbidden.css @@ -0,0 +1,8 @@ +.container { + max-width: 1280px; + margin: 0 auto; +} + +.copy { + padding: 20px 0; +} \ No newline at end of file diff --git a/client/coral-admin/src/components/Forbidden.js b/client/coral-admin/src/components/Forbidden.js new file mode 100644 index 000000000..0c50e6d76 --- /dev/null +++ b/client/coral-admin/src/components/Forbidden.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './Forbidden.css'; + +const Forbidden = () => ( +
+

+ This page is for team use only. Please contact an administrator if you + want to join this team. +

+
+); + +export default Forbidden; diff --git a/client/coral-admin/src/components/IfHasLink.js b/client/coral-admin/src/components/IfHasLink.js index 8209a28e5..35be8c967 100644 --- a/client/coral-admin/src/components/IfHasLink.js +++ b/client/coral-admin/src/components/IfHasLink.js @@ -1,5 +1,5 @@ import React from 'react'; -import { matchLinks } from '../utils'; +import matchLinks from 'coral-framework/utils/matchLinks'; export default ({ text, children }) => { const hasLinks = !!matchLinks(text); diff --git a/client/coral-admin/src/components/Layout.css b/client/coral-admin/src/components/Layout.css index ac13e96f7..9244c7c80 100644 --- a/client/coral-admin/src/components/Layout.css +++ b/client/coral-admin/src/components/Layout.css @@ -1,4 +1,6 @@ .layout { margin: 0 auto; background-color: #FAFAFA; -} + height: inherit; + min-height: calc(100vh - 58px); +} \ No newline at end of file diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index 72c85c960..f2ff2e932 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -5,7 +5,7 @@ import { Link } from 'react-router'; import { Icon } from 'coral-ui'; import CommentDetails from './CommentDetails'; import styles from './UserDetailComment.css'; -import CommentFormatter from 'coral-admin/src/components/CommentFormatter'; +import AdminCommentContent from 'coral-framework/components/AdminCommentContent'; import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; import CommentAnimatedEdit from './CommentAnimatedEdit'; @@ -93,7 +93,7 @@ class UserDetailComment extends React.Component { 'talk-admin-user-detail-comment' )} size={1} - defaultComponent={CommentFormatter} + defaultComponent={AdminCommentContent} passthrough={slotPassthrough} /> -

- This page is for team use only. Please contact an administrator if - you want to join this team. -

+ ); } diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index bc5780c0a..28dcf8ca8 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -8,7 +8,7 @@ import styles from './Comment.css'; import CommentLabels from 'coral-admin/src/components/CommentLabels'; import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit'; import Slot from 'coral-framework/components/Slot'; -import CommentFormatter from 'coral-admin/src/components/CommentFormatter'; +import AdminCommentContent from 'coral-framework/components/AdminCommentContent'; import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; import ApproveButton from 'coral-admin/src/components/ApproveButton'; @@ -140,7 +140,7 @@ class Comment extends React.Component { fill="adminCommentContent" className={cn(styles.commentContent, 'talk-admin-comment')} size={1} - defaultComponent={CommentFormatter} + defaultComponent={AdminCommentContent} passthrough={{ ...slotPassthrough, ...formatterSettings }} />
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css index 465c7ccbd..cef4184db 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css @@ -6,10 +6,6 @@ margin-top: 16px; } -:global(html) { - height: inherit; -} - .list { outline: none; } diff --git a/client/coral-admin/src/utils/index.js b/client/coral-admin/src/utils/index.js index 41ff3db3c..180615a87 100644 --- a/client/coral-admin/src/utils/index.js +++ b/client/coral-admin/src/utils/index.js @@ -1,12 +1,3 @@ -import LinkifyIt from 'linkify-it'; -import tlds from 'tlds'; -const linkify = new LinkifyIt(); -linkify.tlds(tlds); - -export function matchLinks(text) { - return linkify.match(text); -} - export const isPremod = mod => mod === 'PRE'; export const getModPath = (type = 'all', assetId) => diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index bc31c90e9..0f43ffe10 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -71,16 +71,19 @@ export const setActiveTab = tab => dispatch => { dispatch({ type: actions.SET_ACTIVE_TAB, tab }); }; +// @Deprecated export const addCommentBoxTag = tag => ({ type: actions.ADD_COMMENT_BOX_TAG, tag, }); +// @Deprecated export const removeCommentBoxTag = idx => ({ type: actions.REMOVE_COMMENT_BOX_TAG, idx, }); +// @Deprecated export const clearCommentBoxTags = () => ({ type: actions.CLEAR_COMMENT_BOX_TAGS, }); diff --git a/client/coral-embed-stream/src/containers/ExtendableTabPanel.js b/client/coral-embed-stream/src/containers/ExtendableTabPanel.js index 678981a8e..c2935b152 100644 --- a/client/coral-embed-stream/src/containers/ExtendableTabPanel.js +++ b/client/coral-embed-stream/src/containers/ExtendableTabPanel.js @@ -35,12 +35,9 @@ class ExtendableTabPanelContainer extends React.Component { createPluginTabFactory = (props = this.props) => el => { return ( - + {React.cloneElement(el, { - active: props.activeTab === el.type.talkPluginName, + active: props.activeTab === el.key, })} ); @@ -59,7 +56,7 @@ class ExtendableTabPanelContainer extends React.Component { createPluginTabPane(el) { return ( - + {el} ); diff --git a/client/coral-embed-stream/src/reducers/configure.js b/client/coral-embed-stream/src/reducers/configure.js index 41d87f8d8..48b28ec72 100644 --- a/client/coral-embed-stream/src/reducers/configure.js +++ b/client/coral-embed-stream/src/reducers/configure.js @@ -8,7 +8,7 @@ const initialState = { errors: {}, }; -export default function config(state = initialState, action) { +export default function configure(state = initialState, action) { switch (action.type) { case actions.UPDATE_PENDING: { let next = state; diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js index ae60770d4..64593fc43 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js +++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js @@ -1,39 +1,15 @@ import React from 'react'; import QuestionBox from '../../../components/QuestionBox'; -import { Icon, Spinner } from 'coral-ui'; import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon'; import cn from 'classnames'; import styles from './QuestionBoxBuilder.css'; +import { Icon } from 'coral-ui'; +import MarkdownEditor from 'coral-framework/components/MarkdownEditor'; const DefaultIcon = ; - const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote']; class QuestionBoxBuilder extends React.Component { - constructor() { - super(); - - this.state = { - loading: true, - }; - } - - componentWillMount() { - this.loadEditor(); - } - - async loadEditor() { - const { - default: MarkdownEditor, - } = await import(/* webpackChunkName: "markdownEditor" */ - 'coral-framework/components/MarkdownEditor'); - - return this.setState({ - loading: false, - MarkdownEditor, - }); - } - render() { const { questionBoxIcon, @@ -41,11 +17,6 @@ class QuestionBoxBuilder extends React.Component { onContentChange, onIconChange, } = this.props; - const { loading, MarkdownEditor } = this.state; - - if (loading) { - return ; - } return (
diff --git a/client/coral-embed-stream/src/tabs/profile/components/Comment.css b/client/coral-embed-stream/src/tabs/profile/components/Comment.css index 40a003de1..9004a1e6c 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/Comment.css +++ b/client/coral-embed-stream/src/tabs/profile/components/Comment.css @@ -15,6 +15,7 @@ .main { min-width: 70%; + max-width: 100%; } .sidebar { diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js index 6165d126b..5df119fbc 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues'; import get from 'lodash/get'; import LoadMore from './LoadMore'; -import { getEditableUntilDate } from './util'; +import { getEditableUntilDate } from '../util'; import { findCommentWithId } from '../../../graphql/utils'; import CommentContent from 'coral-framework/components/CommentContent'; import Slot from 'coral-framework/components/Slot'; diff --git a/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js b/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js index 65937b3ce..a65950d7d 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js +++ b/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js @@ -18,14 +18,8 @@ class CommentForm extends React.Component { charCountEnable: PropTypes.bool.isRequired, maxCharCount: PropTypes.number, - // 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, + // Unique identifier for this form + id: PropTypes.string, // render at start of button container (useful for extra buttons) buttonContainerStart: PropTypes.node, @@ -37,15 +31,15 @@ class CommentForm extends React.Component { submitButtonCStyle: PropTypes.string, // return whether the submit button should be enabled for the provided - // comment ({ body }) (for reasons other than charCount) + // input (for reasons other than charCount) submitEnabled: PropTypes.func, // className to add to buttons submitButtonClassName: PropTypes.string, cancelButtonClassName: PropTypes.string, - body: PropTypes.string.isRequired, - onBodyChange: PropTypes.func.isRequired, + input: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func, state: PropTypes.string, @@ -53,13 +47,12 @@ class CommentForm extends React.Component { registerHook: PropTypes.func, unregisterHook: PropTypes.func, isReply: PropTypes.bool, + isEdit: PropTypes.bool, root: PropTypes.object.isRequired, comment: PropTypes.object, }; static get defaultProps() { return { - bodyLabel: t('comment_box.comment'), - bodyPlaceholder: t('comment_box.comment'), submitText: t('comment_box.post'), submitButtonCStyle: 'darkGrey', submitEnabled: () => true, @@ -90,20 +83,20 @@ class CommentForm extends React.Component { cancelButtonClassName, submitButtonClassName, charCountEnable, - body, + input, loadingState, comment, root, } = this.props; - const length = body.length; + const length = input.body.length; const isRespectingMaxCount = length => charCountEnable && maxCharCount && length > maxCharCount; const disableSubmitButton = !length || - body.trim().length === 0 || + input.body.trim().length === 0 || isRespectingMaxCount(length) || - !submitEnabled({ body }) || + !submitEnabled(input) || loadingState === 'loading'; const disableCancelButton = loadingState === 'loading'; const disableTextArea = loadingState === 'loading'; @@ -113,17 +106,16 @@ class CommentForm extends React.Component {
{this.props.buttonContainerStart} diff --git a/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js b/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js index 7150a124d..60bf18b40 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js +++ b/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js @@ -13,17 +13,17 @@ import styles from './DraftArea.css'; */ export default class DraftArea extends React.Component { renderCharCount() { - const { value, maxCharCount } = this.props; + const { input, maxCharCount } = this.props; const className = cn( styles.charCount, 'talk-plugin-commentbox-char-count', { [`${styles.charMax} talk-plugin-commentbox-char-max`]: - value.length > maxCharCount, + input.body.length > maxCharCount, } ); - const remaining = maxCharCount - value.length; + const remaining = maxCharCount - input.body.length; return (
@@ -32,18 +32,30 @@ export default class DraftArea extends React.Component { ); } + getLabel() { + if (this.props.isEdit) { + return t('edit_comment.body_input_label'); + } + return this.props.isReply ? t('comment_box.reply') : t('comment.comment'); + } + + getPlaceholder() { + if (this.props.isEdit) { + return ''; + } + return this.getLabel(); + } + render() { const { - value, - placeholder, + input, id, disabled, - rows, - label, charCountEnable, maxCharCount, - onChange, + onInputChange, isReply, + isEdit, registerHook, unregisterHook, root, @@ -51,31 +63,30 @@ export default class DraftArea extends React.Component { } = this.props; return ( -
+
- + {/* Is this slot here legitimate? (kiwi) */}
{charCountEnable && maxCharCount > 0 && this.renderCharCount()} @@ -84,23 +95,17 @@ export default class DraftArea extends React.Component { } } -DraftArea.defaultProps = { - rows: 3, -}; - DraftArea.propTypes = { charCountEnable: PropTypes.bool, maxCharCount: PropTypes.number, id: PropTypes.string, - value: PropTypes.string, - placeholder: PropTypes.string, - label: PropTypes.string, - onChange: PropTypes.func, + input: PropTypes.object, + onInputChange: PropTypes.func, disabled: PropTypes.bool, - rows: PropTypes.number, root: PropTypes.object.isRequired, comment: PropTypes.object, registerHook: PropTypes.func, unregisterHook: PropTypes.func, isReply: PropTypes.bool, + isEdit: PropTypes.bool, }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js b/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js index 50a481f6f..74e59f795 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js +++ b/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js @@ -3,36 +3,49 @@ import PropTypes from 'prop-types'; import cn from 'classnames'; import styles from './DraftAreaContent.css'; -const DraftAreaContent = ({ - value, - placeholder, - id, - onChange, - rows, - disabled, -}) => ( -