From 95934b87a4895a69e44b407938f46c38c433048f Mon Sep 17 00:00:00 2001 From: okbel Date: Wed, 21 Mar 2018 15:43:40 -0300 Subject: [PATCH 01/21] Adding docs for plugins api --- docs/source/05-03-Plugins-API.md | 185 +++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 docs/source/05-03-Plugins-API.md diff --git a/docs/source/05-03-Plugins-API.md b/docs/source/05-03-Plugins-API.md new file mode 100644 index 000000000..22d44c004 --- /dev/null +++ b/docs/source/05-03-Plugins-API.md @@ -0,0 +1,185 @@ +# Plugins API +This is a set of utilities that we expose to create or add functionality to the Plugins. Feel free to check all the utilities here at `talk/plugin-api`. + +## Actions +### Import +``` +import {notify} 'plugin-api/beta/actions'; +``` + +#### Admin +* `viewUserDetail` + +#### Auth +* `setAuthToken` +* `handleSuccessfulLogin` +* `logout` + +#### Notification +* `notify` + +#### Stream +* `setSort` +* `showSignInDialog`` + +## Components +### Import +``` +import {Slot} 'plugin-api/beta/components'; +``` + + +* `Slot` +You probably won’t need to use the `` component in your plugin. But there’s a chance you might want to add a Slot so another plugin gets injected in your plugin. + +```js +const slotPassthrough = { + clearHeightCache, + root, + asset, + comment, +}; + + +``` + +* `IfSlotIsEmpty` + +```js + +``` + +* `IfSlotIsNotEmpty` +* `ClickOutside` +* `CommentAuthorName` +* `CommentTimestamp` +* `CommentDetail` +* `CommentContent` +* `ConfigureCard` +* `StreamConfiguration` +* `Recaptcha` + +## HOCS - Higher Order Components +### Import +``` +import {withReaction} 'plugin-api/beta/hoc'; +``` + +### Hocs +*`withGraphQLExtension`* + +This HOC allows components to register GraphQLExtensions for the framework. IMPORTANT: The extensions are only picked up when the component is used in a slot. + +```js +import {withGraphQLExtension} 'plugin-api/beta/hoc'; + +// MyComponent.js +withGraphQLExtension({ + mutations: { + UpdateNotificationSettings: () => ({ + update: proxy => {...} + }) + }, + fragments: {...}, + query: {...}, +})(MyComponent); +``` + +And then update your `my-plugin/client/index.js` + +```js +export default { + mySlot: [MyComponent], +} +``` + +* `withReaction` +Provides you utilities to create components that interact with Reactions. + +* `withTags` +Provides you utilities to create components that interact with Tags. + +* `withSortOption` +* `withEmit` +* `excludeIf` +* `withFragments` +* `withMutation` +* `withForgotPassword` +* `withSignIn` +* `withSignUp` +* `withResendEmailConfirmation` +* `withSetUsername` +* `withEnumValues` +* `withVariables` +* `withFetchMore` +* `withSubscribeToMore` +* `withRefetch` +* `withIgnoreUser` +* `withBanUser` +* `withUnbanUser` +* `withStopIgnoringUser` +* `withSetCommentStatus` +* `compose` + +## Coral UI +### Import +``` +import {Button} 'plugin-api/beta/components/ui'; +``` + +### Components +* `Alert` +* `Dialog` +* `CoralLogo` +* `FabButton` +* `TabBar` +* `Tab` +* `TabCount` +* `TabContent` +* `TabPane` +* `Button` +* `Spinner` +* `Tooltip` +* `PopupMenu` +* `Checkbox` +* `Icon` +* `List` +* `Item` +* `Card` +* `TextField` +* `Success` +* `Paginate` +* `Wizard` +* `WizardNav` +* `SnackBar` +* `TextArea` +* `Drawer` +* `Label` +* `FlagLabel` +* `Dropdown` +* `Option` +* `BareButton` + +## Services +### Import +``` +import {t, timeago, can} 'plugin-api/beta/services'; +``` + +* `t` +To manage translations. + +* `timeago` +Handle time with [timeago](https://github.com/hustcc/timeago.js) + +* `can` +A permissions utility. From 24f9afd531d3fbb477cc7c0cc4bf83a0f390fcbc Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 21 Mar 2018 15:06:20 -0400 Subject: [PATCH 02/21] Update sidebar and add title info --- docs/_config.yml | 2 ++ docs/source/{05-03-Plugins-API.md => 04-07-plugins-api.md} | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) rename docs/source/{05-03-Plugins-API.md => 04-07-plugins-api.md} (98%) diff --git a/docs/_config.yml b/docs/_config.yml index f524fed0a..027e25921 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -138,6 +138,8 @@ sidebar: url: /plugin-recipes/ - title: Slots and Plugins url: /slots-and-plugins/ + - title: Plugins API + url: /plugins-api/ - title: Tutorials children: - title: Creating a Basic Plugin diff --git a/docs/source/05-03-Plugins-API.md b/docs/source/04-07-plugins-api.md similarity index 98% rename from docs/source/05-03-Plugins-API.md rename to docs/source/04-07-plugins-api.md index 22d44c004..aea655cb5 100644 --- a/docs/source/05-03-Plugins-API.md +++ b/docs/source/04-07-plugins-api.md @@ -1,4 +1,8 @@ -# Plugins API +--- +title: Plugins API +permalink: /plugins-api/ +--- + This is a set of utilities that we expose to create or add functionality to the Plugins. Feel free to check all the utilities here at `talk/plugin-api`. ## Actions From 773c8438341bcaeee375ff90b76bc87659b934ef Mon Sep 17 00:00:00 2001 From: okbel Date: Thu, 22 Mar 2018 11:59:33 -0300 Subject: [PATCH 03/21] Click Outside --- docs/source/05-03-Plugins-API.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/source/05-03-Plugins-API.md b/docs/source/05-03-Plugins-API.md index 22d44c004..4c90a0aa2 100644 --- a/docs/source/05-03-Plugins-API.md +++ b/docs/source/05-03-Plugins-API.md @@ -1,7 +1,10 @@ # Plugins API -This is a set of utilities that we expose to create or add functionality to the Plugins. Feel free to check all the utilities here at `talk/plugin-api`. +We created a set of utilities to make it easier to create and add functionality to plugins. + +Feel free to check all the utilities here: `talk/plugin-api`. ## Actions + ### Import ``` import {notify} 'plugin-api/beta/actions'; @@ -59,7 +62,26 @@ const slotPassthrough = { ``` * `IfSlotIsNotEmpty` + * `ClickOutside` +This utility handle click events outside the component. + +### Props +* `onClickOutside` : Takes handler function + +#### Import +```js +import { ClickOutside } from 'plugin-api/beta/client/components'; +``` + +#### Usage +```js + + // Your component + +``` + + * `CommentAuthorName` * `CommentTimestamp` * `CommentDetail` From 1fc750083742023224b0acfac85ba9f2a7223d31 Mon Sep 17 00:00:00 2001 From: okbel Date: Thu, 22 Mar 2018 12:36:46 -0300 Subject: [PATCH 04/21] More --- docs/source/05-03-Plugins-API.md | 162 ++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 37 deletions(-) diff --git a/docs/source/05-03-Plugins-API.md b/docs/source/05-03-Plugins-API.md index 4c90a0aa2..c15b040cd 100644 --- a/docs/source/05-03-Plugins-API.md +++ b/docs/source/05-03-Plugins-API.md @@ -1,15 +1,8 @@ # Plugins API We created a set of utilities to make it easier to create and add functionality to plugins. - Feel free to check all the utilities here: `talk/plugin-api`. ## Actions - -### Import -``` -import {notify} 'plugin-api/beta/actions'; -``` - #### Admin * `viewUserDetail` @@ -25,16 +18,46 @@ import {notify} 'plugin-api/beta/actions'; * `setSort` * `showSignInDialog`` +### Import +``` +import {notify} 'plugin-api/beta/actions'; +``` + +### Usage +```js +// Trigger a notification +notify('success', t('suspenduser.notify_suspend_until', username, timeago(until)) + +// mapDispatchToProps +const mapDispatchToProps = dispatch => ({ + ...bindActionCreators( + { + notify, + }, + dispatch + ), +}); + +``` + + ## Components +* `Slot` +You probably won’t need to use the `` component in your plugin. But there’s a chance you might want to add a Slot so another plugin gets injected in your plugin. + +### Props +* `fill ` : Name of the slot +* `defaultComponent` : The default component if no plugin component is provided to the Slot +* `size` : - How many components this Slot should show - Slot size or an Array of slot size +* `passthrough`: - The properties that you want to pass to the Slot, therefore to the plugins. +* `className` : - Slot’s class name + ### Import ``` import {Slot} 'plugin-api/beta/components'; ``` - -* `Slot` -You probably won’t need to use the `` component in your plugin. But there’s a chance you might want to add a Slot so another plugin gets injected in your plugin. - +### Usage ```js const slotPassthrough = { clearHeightCache, @@ -54,6 +77,12 @@ const slotPassthrough = { * `IfSlotIsEmpty` +### Import +``` +import {IfSlotIsEmpty} 'plugin-api/beta/components'; +``` + +### Usage ```js +``` + * `ClickOutside` This utility handle click events outside the component. @@ -81,7 +123,6 @@ import { ClickOutside } from 'plugin-api/beta/client/components'; ``` - * `CommentAuthorName` * `CommentTimestamp` * `CommentDetail` @@ -91,20 +132,17 @@ import { ClickOutside } from 'plugin-api/beta/client/components'; * `Recaptcha` ## HOCS - Higher Order Components -### Import -``` -import {withReaction} 'plugin-api/beta/hoc'; -``` - -### Hocs *`withGraphQLExtension`* This HOC allows components to register GraphQLExtensions for the framework. IMPORTANT: The extensions are only picked up when the component is used in a slot. +### Import ```js -import {withGraphQLExtension} 'plugin-api/beta/hoc'; +import { withGraphQLExtension } from 'plugin-api/beta/hoc'; +``` -// MyComponent.js +### Usage +```js withGraphQLExtension({ mutations: { UpdateNotificationSettings: () => ({ @@ -127,9 +165,33 @@ export default { * `withReaction` Provides you utilities to create components that interact with Reactions. +Check this tutorial to know more about the usage of `withReaction` [Creating a Basic Pride Reaction Plugin | Talk Documentation](https://docs.coralproject.net/talk/building-basic-plugin/) + +### Import +```js +import { withReaction } from 'plugin-api/beta/hoc'; +``` + +### Usage +```js +export default withReaction('pride')(PrideButton); +``` + + * `withTags` Provides you utilities to create components that interact with Tags. +### Import +```js +import { withTags } from 'plugin-api/beta/hoc'; +``` + +### Usage +```js +export default withTags('featured')(FeaturedButton); +``` + + * `withSortOption` * `withEmit` * `excludeIf` @@ -152,9 +214,50 @@ Provides you utilities to create components that interact with Tags. * `withSetCommentStatus` * `compose` -## Coral UI -### Import +## Services + +* `t` +To manage translations. + +### Import +```js +import { t } from 'coral-framework/services/perms'; ``` + +* `timeago` +Handle time with [timeago](https://github.com/hustcc/timeago.js) + +### Import +```js +import { timeago } from 'coral-framework/services/perms'; +``` + +* `can` +A permissions utility. + +### Import +```js +import { can } from 'coral-framework/services/perms'; +``` + +### Usage +```js +{can(currentUser, 'UPDATE_CONFIG') && ( + + {t('configure.configure')} + +)} +``` + +## Coral UI +Coral UI is a set of components to help you build your UI. This powers our core. + +### Import +```js import {Button} 'plugin-api/beta/components/ui'; ``` @@ -190,18 +293,3 @@ import {Button} 'plugin-api/beta/components/ui'; * `Dropdown` * `Option` * `BareButton` - -## Services -### Import -``` -import {t, timeago, can} 'plugin-api/beta/services'; -``` - -* `t` -To manage translations. - -* `timeago` -Handle time with [timeago](https://github.com/hustcc/timeago.js) - -* `can` -A permissions utility. From 36988d7e203482fca95ca31bd43dd174f706dc3c Mon Sep 17 00:00:00 2001 From: okbel Date: Thu, 22 Mar 2018 15:29:56 -0300 Subject: [PATCH 05/21] Forbidden component --- client/coral-admin/src/components/Forbidden.css | 8 ++++++++ client/coral-admin/src/components/Forbidden.js | 13 +++++++++++++ client/coral-admin/src/containers/Layout.js | 6 ++---- .../Moderation/components/ModerationQueue.css | 4 ---- views/admin.ejs | 11 ++++++----- 5 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 client/coral-admin/src/components/Forbidden.css create mode 100644 client/coral-admin/src/components/Forbidden.js 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/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index aad9971c4..580b20f9b 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -11,6 +11,7 @@ import { logout } from 'coral-framework/actions/auth'; import { can } from 'coral-framework/services/perms'; import UserDetail from 'coral-admin/src/containers/UserDetail'; import PropTypes from 'prop-types'; +import Forbidden from '../components/Forbidden'; class LayoutContainer extends React.Component { render() { @@ -47,10 +48,7 @@ class LayoutContainer extends React.Component { } else { return ( -

- 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/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/views/admin.ejs b/views/admin.ejs index 1ecca390e..a5e05032e 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -4,17 +4,18 @@ Talk - Coral Admin From b0ea4960e36660bcdb7ad9988a94d8405a5a9cbf Mon Sep 17 00:00:00 2001 From: okbel Date: Thu, 22 Mar 2018 15:40:04 -0300 Subject: [PATCH 06/21] Working --- client/coral-admin/src/components/Layout.css | 4 +++- views/admin.ejs | 14 ++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/client/coral-admin/src/components/Layout.css b/client/coral-admin/src/components/Layout.css index ac13e96f7..1b3f1d811 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: 100vh; +} \ No newline at end of file diff --git a/views/admin.ejs b/views/admin.ejs index a5e05032e..979e2ec41 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -4,18 +4,8 @@ Talk - Coral Admin From 34fa2fb89114d2a477e70da9873ccb46e11bb318 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 12:50:37 -0600 Subject: [PATCH 07/21] Verification link copy --- errors.js | 11 +++++++++++ locales/en.yml | 1 + routes/api/v1/account.js | 13 +++++++++++-- services/users.js | 4 ++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/errors.js b/errors.js index 65f0a0d2b..53e8bf351 100644 --- a/errors.js +++ b/errors.js @@ -97,6 +97,16 @@ const ErrEmailVerificationToken = new APIError('token is required', { status: 400, }); +// ErrEmailAlreadyVerified is returned when the user tries to verify an email +// address that has already been verified. +const ErrEmailAlreadyVerified = new APIError( + 'email address is already verified', + { + translation_key: 'EMAIL_ALREADY_VERIFIED', + status: 409, + } +); + // ErrPasswordResetToken is returned in the event that the password reset is requested // without a token. const ErrPasswordResetToken = new APIError('token is required', { @@ -284,6 +294,7 @@ module.exports = { ErrCommentTooShort, ErrContainsProfanity, ErrEditWindowHasEnded, + ErrEmailAlreadyVerified, ErrEmailTaken, ErrEmailVerificationToken, ErrInstallLock, diff --git a/locales/en.yml b/locales/en.yml index 59689d1b1..08d64ddc8 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -206,6 +206,7 @@ en: error: COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist." EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." + EMAIL_ALREADY_VERIFIED: "Email address already verified." PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid." COMMENT_TOO_SHORT: "Comments should be more than one character, please revise your comment and try again." NOT_AUTHORIZED: "You are not authorized to perform this action." diff --git a/routes/api/v1/account.js b/routes/api/v1/account.js index f7d8ea126..561134885 100644 --- a/routes/api/v1/account.js +++ b/routes/api/v1/account.js @@ -17,7 +17,11 @@ router.get('/', authorization.needed(), (req, res, next) => { * @param {Function} verifier the function used to verify the token, will throw on error * @param {Object} error the error object to send back in the event an error is found */ -const tokenCheck = (verifier, error) => async (req, res, next) => { +const tokenCheck = (verifier, error, ...whitelistedErrors) => async ( + req, + res, + next +) => { const { token = null, check = false } = req.body; if (check) { @@ -26,6 +30,10 @@ const tokenCheck = (verifier, error) => async (req, res, next) => { // Verify the token. await verifier(token); } catch (err) { + if (whitelistedErrors.includes(err)) { + return next(err); + } + // Log out the error, slurp it and send out the predefined error to the // error handler. console.error(err); @@ -48,7 +56,8 @@ router.post( '/email/verify', tokenCheck( UsersService.verifyEmailConfirmationToken, - errors.ErrEmailVerificationToken + errors.ErrEmailVerificationToken, + errors.ErrEmailAlreadyVerified ), async (req, res, next) => { const { token } = req.body; diff --git a/services/users.js b/services/users.js index af91cacc7..0617d5158 100644 --- a/services/users.js +++ b/services/users.js @@ -837,7 +837,7 @@ class UsersService { // Ensure that the user email hasn't already been verified. if (profile && profile.metadata && profile.metadata.confirmed_at) { - throw new Error('email address already confirmed'); + throw errors.ErrEmailAlreadyVerified; } return JWT_SECRET.sign( @@ -884,7 +884,7 @@ class UsersService { } if (profile.metadata && profile.metadata.confirmed_at !== null) { - throw errors.ErrEmailVerificationToken; + throw errors.ErrEmailAlreadyVerified; } return decoded; From 61836423acf8fdb126086c63564b4ee5c1573ef7 Mon Sep 17 00:00:00 2001 From: okbel Date: Thu, 22 Mar 2018 16:21:13 -0300 Subject: [PATCH 08/21] Adding CommentNotFound Component --- .../src/tabs/stream/components/Stream.js | 11 +++--- .../tabs/stream/containers/CommentNotFound.js | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 client/coral-embed-stream/src/tabs/stream/containers/CommentNotFound.js diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index 75f04da0a..cc4e8e939 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -15,14 +15,13 @@ import QuestionBox from '../../../components/QuestionBox'; import { Tab, TabCount, TabPane } from 'coral-ui'; import cn from 'classnames'; import get from 'lodash/get'; - import { reverseCommentParentTree } from '../../../graphql/utils'; import AllCommentsPane from './AllCommentsPane'; import ExtendableTabPanel from '../../../containers/ExtendableTabPanel'; +import ChangedUsername from './ChangedUsername'; +import CommentNotFound from '../containers/CommentNotFound'; import styles from './Stream.css'; -import ChangedUsername from './ChangedUsername'; - class Stream extends React.Component { constructor(props) { super(props); @@ -238,7 +237,11 @@ class Stream extends React.Component { keepCommentBox); if (highlightedComment === null) { - return {t('stream.comment_not_found')}; + return ( + + + + ); } const slotPassthrough = { root, asset }; diff --git a/client/coral-embed-stream/src/tabs/stream/containers/CommentNotFound.js b/client/coral-embed-stream/src/tabs/stream/containers/CommentNotFound.js new file mode 100644 index 000000000..91537aac4 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/stream/containers/CommentNotFound.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'coral-ui'; +import { setActiveTab } from '../../../actions/embed'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import t from 'coral-framework/services/i18n'; + +class CommentNotFound extends React.Component { + showAllTab = () => { + this.props.setActiveTab('all'); + }; + + render() { + return ( +
+

{t('stream.comment_not_found')}

+ +
+ ); + } +} + +CommentNotFound.propTypes = { + setActiveTab: PropTypes.func, +}; + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + setActiveTab, + }, + dispatch + ); + +export default connect(null, mapDispatchToProps)(CommentNotFound); From 752c16f513135187bfb5a997d50086b228b4168e Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 22 Mar 2018 18:35:14 -0400 Subject: [PATCH 09/21] Move Plugin Client and Slots docs to API section --- docs/_config.yml | 20 +++++++++---------- .../{04-07-plugins-api.md => api/client.md} | 7 +++++-- docs/source/api/server.md | 2 +- .../slots.md} | 6 ++++-- 4 files changed, 20 insertions(+), 15 deletions(-) rename docs/source/{04-07-plugins-api.md => api/client.md} (98%) rename docs/source/{04-06-slots-and-plugins.md => api/slots.md} (98%) diff --git a/docs/_config.yml b/docs/_config.yml index 506bd2542..f269d8695 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -136,16 +136,6 @@ sidebar: url: /plugins-directory/ - title: Plugin Recipes url: /plugin-recipes/ - - title: Slots and Plugins - url: /slots-and-plugins/ - - title: Plugins API - url: /plugins-api/ - - title: Tutorials - children: - - title: Creating a Basic Plugin - url: /building-basic-plugin/ - - title: Customizing Plugins with Coral UI - url: /customizing-plugins-coral-ui/ - title: API children: - title: GraphQL Overview @@ -154,6 +144,16 @@ sidebar: url: /api/graphql/ - title: Server Plugin API url: /api/server/ + - title: Client Plugin API + url: /api/client/ + - title: Plugin Slots API + url: /api/slots/ + - title: Tutorials + children: + - title: Creating a Basic Plugin + url: /building-basic-plugin/ + - title: Customizing Plugins with Coral UI + url: /customizing-plugins-coral-ui/ - title: Migrating children: - title: Migrating to v4.0.0 diff --git a/docs/source/04-07-plugins-api.md b/docs/source/api/client.md similarity index 98% rename from docs/source/04-07-plugins-api.md rename to docs/source/api/client.md index 2f12fccf5..fd0ec42fc 100644 --- a/docs/source/04-07-plugins-api.md +++ b/docs/source/api/client.md @@ -1,7 +1,10 @@ --- -title: Plugins API -permalink: /plugins-api/ +title: Client Plugin API +permalink: /api/client/ +toc: true +class: configuration --- + We created a set of utilities to make it easier to create and add functionality to plugins. Feel free to check all the utilities here: `talk/plugin-api`. diff --git a/docs/source/api/server.md b/docs/source/api/server.md index 614c7b2ed..821b4b24f 100644 --- a/docs/source/api/server.md +++ b/docs/source/api/server.md @@ -524,4 +524,4 @@ module.exports = { } }; -``` \ No newline at end of file +``` diff --git a/docs/source/04-06-slots-and-plugins.md b/docs/source/api/slots.md similarity index 98% rename from docs/source/04-06-slots-and-plugins.md rename to docs/source/api/slots.md index 907c85862..4ac47a516 100644 --- a/docs/source/04-06-slots-and-plugins.md +++ b/docs/source/api/slots.md @@ -1,6 +1,8 @@ --- -title: Slots and Plugins -permalink: /slots-and-plugins/ +title: Plugin Slots API +permalink: /api/slots/ +toc: true +class: configuration --- Plugins make use of **"slots"** in order to change Talk's interface. From 3ec7bf09bf25e102ce5037d2cc4805b609c12500 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 17:28:44 -0600 Subject: [PATCH 10/21] improved introspection --- .nodemon.json | 2 +- docs/.gitignore | 3 +- docs/source/api/graphql.md | 2 +- scripts/generateIntrospectionResult.js | 151 +++++++++++++++++++++---- 4 files changed, 135 insertions(+), 23 deletions(-) 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/docs/.gitignore b/docs/.gitignore index b9fd845b9..be0603043 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,4 +2,5 @@ public/* !public/_redirects .deploy*/ db.json -*.log \ No newline at end of file +*.log +source/_data/introspection.json \ No newline at end of file diff --git a/docs/source/api/graphql.md b/docs/source/api/graphql.md index 792f2b240..4275fe7f0 100644 --- a/docs/source/api/graphql.md +++ b/docs/source/api/graphql.md @@ -12,4 +12,4 @@ interact with Talk's GraphQL endpoint. # GraphQL Schema -{% graphqldocs ../../client/coral-framework/graphql/introspection.json %} \ No newline at end of file +{% graphqldocs _data/introspection.json %} \ No newline at end of file diff --git a/scripts/generateIntrospectionResult.js b/scripts/generateIntrospectionResult.js index e07fe7c6d..7c3ea024c 100755 --- a/scripts/generateIntrospectionResult.js +++ b/scripts/generateIntrospectionResult.js @@ -1,7 +1,120 @@ #! /usr/bin/env node const path = require('path'); -const introspectionFilename = path.resolve( +const fs = require('fs'); +const { graphql } = require('graphql'); +const schema = require('../graph/schema'); + +// Copied from https://github.com/graphql/graphql-js/blob/f995c1f92e94d9c451104b6a0db8034165ef8640/src/utilities/introspectionQuery.js#L18-L113 +// which is available in graphql@0.13.2 +// +// TODO: remove when we upgrade to at least graphql@0.13.2. +function getIntrospectionQuery(options = {}) { + const descriptions = !(options && options.descriptions === false); + return ` + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + ${descriptions ? 'description' : ''} + locations + args { + ...InputValue + } + } + } + } + fragment FullType on __Type { + kind + name + ${descriptions ? 'description' : ''} + fields(includeDeprecated: true) { + name + ${descriptions ? 'description' : ''} + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + ${descriptions ? 'description' : ''} + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment InputValue on __InputValue { + name + ${descriptions ? 'description' : ''} + type { ...TypeRef } + defaultValue + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + } + `; +} + +const generateIntrospectionResult = (resultLocation, options = {}) => + graphql(schema, getIntrospectionQuery(options)).then(({ data }) => { + // Serialize the introspection result as JSON. + const introspectionResult = JSON.stringify(data, null, 2); + + // Write the introspection result to the filesystem. + fs.writeFileSync(resultLocation, introspectionResult, 'utf8'); + + console.log(`Outputted result of introspectionQuery to ${resultLocation}`); + }); + +const graphIntrospectionFilename = path.resolve( __dirname, '..', 'client', @@ -10,23 +123,21 @@ const introspectionFilename = path.resolve( 'introspection.json' ); -const fs = require('fs'); -const { graphql, introspectionQuery } = require('graphql'); -const schema = require('../graph/schema'); +const docsIntrospectionFilename = path.resolve( + __dirname, + '..', + 'docs', + 'source', + '_data', + 'introspection.json' +); -graphql(schema, introspectionQuery) - .then(({ data }) => { - // Serialize the introspection result as JSON. - const introspectionResult = JSON.stringify(data, null, 2); - - // Write the introspection result to the filesystem. - fs.writeFileSync(introspectionFilename, introspectionResult, 'utf8'); - - console.log( - `Outputted result of introspectionQuery to ${introspectionFilename}` - ); - }) - .catch(err => { - console.error(err); - process.exit(1); - }); +Promise.all([ + generateIntrospectionResult(graphIntrospectionFilename, { + descriptions: false, + }), + generateIntrospectionResult(docsIntrospectionFilename), +]).catch(err => { + console.error(err); + process.exit(1); +}); From 4f87c88e0401058f1ef74a51f786af85a7fdcf10 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 17:29:07 -0600 Subject: [PATCH 11/21] improved static template resolve in development --- middleware/staticTemplate.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js index 01503cca0..0dc05a7be 100644 --- a/middleware/staticTemplate.js +++ b/middleware/staticTemplate.js @@ -66,28 +66,29 @@ function getManifest() { } /** - * resolve is a function that can be used in templates to resolve an asset from - * the manifest. In production, the manifest is cached. + * resolveFactory is a function that can be used in templates to resolve an + * asset from the manifest. In production, the manifest is cached. */ -const resolve = (() => { +const createResolveFactory = (() => { if (process.env.NODE_ENV === 'production') { // In production, we should attempt to load the manifest early. const manifest = getManifest(); - return key => `${STATIC_URL}static/${manifest[key]}`; + return () => key => `${STATIC_URL}static/${manifest[key]}`; } // In dev mode, we are more forgiving and we always load the // newest version of the manifest. - return key => { + return () => { + let manifest = {}; try { - const manifest = getManifest(); - - return `${STATIC_URL}static/${manifest[key]}`; + manifest = getManifest(); } catch (err) { console.warn(err); - return ''; } + + return key => + key in manifest ? `${STATIC_URL}static/${manifest[key]}` : ''; }; })(); @@ -105,7 +106,7 @@ module.exports = async (req, res, next) => { // Resolve will help resolving paths to static files // using the manifest. - res.locals.resolve = resolve; + res.locals.resolve = createResolveFactory(); // Forward the request. next(); From 59982d4329cfe13d90453363a1238322de31ec9f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 17:29:38 -0600 Subject: [PATCH 12/21] swapped manual dynamic chunked loader with readt-loadable --- .../components/QuestionBoxBuilder.js | 90 ++----------------- .../{ => loadable}/QuestionBoxBuilder.css | 0 .../components/loadable/QuestionBoxBuilder.js | 56 ++++++++++++ package.json | 1 + webpack.config.js | 3 +- yarn.lock | 6 ++ 6 files changed, 73 insertions(+), 83 deletions(-) rename client/coral-embed-stream/src/tabs/configure/components/{ => loadable}/QuestionBoxBuilder.css (100%) create mode 100644 client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js 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..e87e7b321 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js +++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js @@ -1,85 +1,11 @@ -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 { Spinner } from 'coral-ui'; +import Loadable from 'react-loadable'; -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, - questionBoxContent, - onContentChange, - onIconChange, - } = this.props; - const { loading, MarkdownEditor } = this.state; - - if (loading) { - return ; - } - - return ( -
-

Include an Icon

- -
    - {icons.map(item => { - const name = typeof item === 'object' ? Object.keys(item)[0] : item; - const icon = typeof item === 'object' ? item[name] : item; - return ( -
  • - -
  • - ); - })} -
- - - - -
- ); - } -} +const QuestionBoxBuilder = Loadable({ + loader: () => + import(/* webpackChunkName: "questionBoxBuilder" */ + './loadable/QuestionBoxBuilder'), + loading: Spinner, +}); export default QuestionBoxBuilder; diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css similarity index 100% rename from client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css rename to client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css diff --git a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js new file mode 100644 index 000000000..837de1280 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js @@ -0,0 +1,56 @@ +import React from 'react'; +import QuestionBox from '../../../../components/QuestionBox'; +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 { + render() { + const { + questionBoxIcon, + questionBoxContent, + onContentChange, + onIconChange, + } = this.props; + + return ( +
+

Include an Icon

+ +
    + {icons.map(item => { + const name = typeof item === 'object' ? Object.keys(item)[0] : item; + const icon = typeof item === 'object' ? item[name] : item; + return ( +
  • + +
  • + ); + })} +
+ + + + +
+ ); + } +} + +export default QuestionBoxBuilder; diff --git a/package.json b/package.json index a807d3321..a3d5f665e 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "react-broadcast": "^0.6.2", "react-dom": "^15.4.2", "react-input-autosize": "^1.1.4", + "react-loadable": "^5.3.1", "react-mdl": "^1.11.0", "react-mdl-selectfield": "^0.2.0", "react-paginate": "^5.0.0", diff --git a/webpack.config.js b/webpack.config.js index 184107491..d6f216d02 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -335,7 +335,8 @@ module.exports = [ { output: { library: 'Coral', - // don't hash the embed. + // don't hash the embed, cache-busting must be completed by the requester + // as this lives in a static template on the embed site. filename: '[name].js', }, plugins: [ diff --git a/yarn.lock b/yarn.lock index 342529006..fd501351e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9217,6 +9217,12 @@ react-linkify@^0.2.1: prop-types "^15.5.8" tlds "^1.57.0" +react-loadable@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/react-loadable/-/react-loadable-5.3.1.tgz#9699e9a08fed49bacd69caaa282034b62a76bcdd" + dependencies: + prop-types "^15.5.0" + react-mdl-selectfield@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/react-mdl-selectfield/-/react-mdl-selectfield-0.2.0.tgz#36e1a97233036c057ab2bdb31ec09ad8d9988411" From 8f727cc6dbb30083f91d4ddb96bcc52e4dd3951b Mon Sep 17 00:00:00 2001 From: Andrew Losowsky Date: Thu, 22 Mar 2018 19:47:29 -0400 Subject: [PATCH 13/21] Create 'when you've installed talk' tutorial --- docs/source/05-03-when-youve-installed-talk | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/source/05-03-when-youve-installed-talk diff --git a/docs/source/05-03-when-youve-installed-talk b/docs/source/05-03-when-youve-installed-talk new file mode 100644 index 000000000..5269e5213 --- /dev/null +++ b/docs/source/05-03-when-youve-installed-talk @@ -0,0 +1,78 @@ +--- +title: What To Do When You've Installed Talk +permalink: /when-youve-installed-talk/ +--- + + +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. **Contents:** + +1. **[Take this opportunity for a fresh start](#1)** +2. **[Publicly state the purpose and rules of your community](#2)** +3. **[Decide where you will and won't put comments](#3)** +4. **[Have clear moderation strategies](#4)** +5. **[Get journalists on your side](#5)** +6. **[Launch with care](#6)** + +### 1\. Take this opportunity for a fresh start + +The launch of a new tool is a great opportunity for a reset, to welcome in new community members, and to make clear what the space is for. [We have a ten-page workbook](https://guides.coralproject.net/workbook/) that you can download/print to help define your goals and vision for the community. It takes about 30 minutes to complete, asks clear, simple questions, and at the end you will have an outline of your community strategy to set you up for success. + +### 2\. Publicly state the purpose and rules of your community + +If you don't launch with a clear strategy for your community, the most disruptive members will end up defining it for you. [Go here to learn how to create an effective community strategy.](https://guides.coralproject.net/write-a-community-mission-statement/) If your community is to succeed, you will need to make clear at the start what is and isn't acceptable, and enforce the rules clearly and consistently. [Read more about that here.](https://guides.coralproject.net/manage-a-successful-community/) Every successful community has an easy-to-read code of conduct, with a summary of the rules on every page that the comments appear. [Here's how to write your code.](https://guides.coralproject.net/create-a-code-of-conduct/) In Talk, the summary of your community code goes into the box at the top of the comments. You enter that text by clicking on the Configure tab at the top, and scroll down to Include Comment Stream Description: ![[IMAGE] A screenshot of the Configure options in Talk, with a pink arrow pointing to the place where Comment Stream Description can be added](http://blog.coralproject.net/wp-content/uploads/2018/03/streamdescription.png) + +### 3\. Decide where you will and won't put comments + +One the most important lessons we wish more newsrooms understood is this: **on-site comments don't have to be all or nothing.** If your goal is to create a civil, productive space for online discourse, you should only make promises about the space that you can keep. If you have very few resources to dedicate to your community, that might mean only opening a small number of articles for discussion each day – or only having a weekly comments discussion about the week's news, similar to [the Guardian Social's Catch Up of the Week where they interact in, and highlight the best of, the comments.](https://www.theguardian.com/commentisfree/live/2017/apr/21/how-do-you-feel-about-another-general-election-join-our-live-look-at-the-week) Some topics that you cover will be more challenging to create civil discourse around than others, and opening the comments on them could require a lot of hands-on moderation to ensure the kinds of interactions that you want – we've found in the U.S. that conversations around issues of race, immigration, and breaking news involving potential assailants or terrorism, can quickly break down and lead to abusive and negative interactions. You will know best which are the most controversial topics in your community, and [you can model the threats you are likely to face as a result.](https://guides.coralproject.net/threat-modeling-for-communities/) For these topics, we recommend that you have a plan ahead of time to address the problems that are likely to come up. Your plans might include: + +* Watching the comments on these articles carefully, and posting a public note using the 'Ask a Question' box stating that you will delete comments, suspend/ban people, or close comments altogether if the conversation devolves (and then following through on that.) Here's where you can add that text from the article page: + +![[IMAGE] A screenshot of the Configure menu from the comments view](http://blog.coralproject.net/wp-content/uploads/2018/03/ask2.png)   + +* Setting the comments on these articles to pre-moderation, then writing a note in the Ask a Question box stating that all comments have to be approved before publication; The Washington Post does this in some breaking news situations. You can set the comments on a single story to pre-moderation via the Configure tab on the article's comments. ![[IMAGE] A screenshot of the pre-moderate option in the Configure tab](http://blog.coralproject.net/wp-content/uploads/2018/03/premod.png) +* Not allowing comments on these topics at all. Following a series of racist responses, for instance, the CBC in Canada chose not to open comments at all on articles about indigenous Canadians. That's a perfectly valid response if you don't have the resources to ensure a civil discussion – if people really want to talk about it, they can go elsewhere.In this situation, we recommend publishing a note at the bottom of all articles where this applies, explaining your decision and directing people to where they can send letters to the editor or continue the discussion off site, so that your community members understand what is happening. + +Other options for contentious topics instead of comments: + +* Use a form to request answers on a specific question related to the article, and select only the best answers for display. This is how the Spotlight team at Boston Globe solicited responses to their series on racism in Boston, using our Ask tool. ([Learn more about Ask here.](https://coralproject.net/products/ask.html)) This also allows people to submit their thoughts anonymously. +* Host a focused, more controlled online discussion about the topic over a fixed period of time, during which you apply more vigilant moderation than usual. You can write a note in the Ask A Question space described above to make that clear, such as "We will host an online conversation about this topic here on Friday between 11am and 3pm EST. Moderators will be present throughout, and our journalists will answer your questions about the topic."This allows community members to approach the topic more calmly than their initial reactions on reading the story, in a space where you can dedicate temporary, more intensive resources to ensuring civility (perhaps with pre-moderation), while still signaling your commitment to engagement around an important topic. + +### 4\. Have clear moderation strategies + +The most important predictors of the success of an online community are: **1\. Does everyone understand and agree with the basic rules?** **2\. Are the rules visibly enforced?** No matter how benign the topic might seem, disruptive behavior will occur in your community (we've seen online communities about classical music and bonsai tree ownership become hotbeds of abuse and aggression.) Whether or not the behavior repeats in your community depends on your response. Once you've created your code of conduct and displayed it clearly (see above), you then need to dedicate resources to moderating your communities **quickly, effectively, and consistently.** + +#### Quickly: + +* Create Banned/Suspect word lists specific to your needs, to prevent the worst words being posted, and to auto-report comments you should keep an eye on.![](http://blog.coralproject.net/wp-content/uploads/2018/03/banned-suspect.jpg)**We have a starter list of more than 1700 words/phrases that most sites choose to ban.** Email support@coralproject.net to request it. +* If you don't have a very high comment volume and use Slack in your newsroom, you could integrate Slack moderation to keep tabs on new/reported comments. [Read more about our free Slack moderation plugin.](https://blog.coralproject.net/slacking-on/) +* Utilize [our Toxic Comments plugin](https://blog.coralproject.net/toxic-avenging/), developed with Google Jigsaw, to improve commenter behavior and use AI to help identify and prevent the most abusive comments from appearing on your site. +* Enable our [Akismet plugin](https://coralproject.github.io/talk/additional-plugins/#talk-plugin-akismet) to keep spam from appearing in your comments. +* Use keyboard shortcuts in the moderation queue to moderate quickly (type '?' in the moderation view to see the list of shortcuts), and if there is a sudden deluge of comments, ask for someone in your newsroom to help you moderate. Talk will notify you in the moderation interface if someone else moderates a comment that's already on your screen. +* Click on a community member's name in the moderation interface to review all their comments, and see if there is a clear pattern of abuse among their Rejected comments. You can also select all their recent comments and delete them in bulk.![A screenshot of the moderation interface with the user drawer on display. Pink arrows indicate the user name, which can be clicked to open the drawer, and the drawer itself.](http://blog.coralproject.net/wp-content/uploads/2018/03/userdrawer.jpg) +* Give people who need to step away from the conversation a 'time out' by suspending their account via the User drawer (click on the user's name anywhere in the moderation interface), and write a personalized note to them to explain why you took this action. Ban any users whose behavior is clearly offensive and/or abusive to an extreme degree.![A close up of the User Drawer with a large pink arrow indicating the Actions menu where users can be suspended or banned](http://blog.coralproject.net/wp-content/uploads/2018/03/suspend-ban.jpg) +* Reject the worst comments on the article page itself by using the small caret in the corner of each comment.![A screenshot of the comments stream with a moderation menu popped out and a large pink arrow pointing to the caret where moderators can unfold the moderation menu](http://blog.coralproject.net/wp-content/uploads/2018/03/caret-mod.jpg) + +* If you find new commenters are causing a lot of trouble with their first comments, consider changing [the User Karma threshold](https://coralproject.github.io/talk/trust/) from negative one to zero, forcing every new commenter's first comment into pre-moderation. + +#### Effectively + +* If you're using the Toxic Comments plugin, make sure that its threshold is set at the level that catches most comments with fewest false positives (default is 80%). You can see the Likely to be Toxic level of every comment by clicking "More Details" on the comment card in the moderation view. +* Publicly discourage behavior in the comments that doesn't cross the line but suggests that the tone or focus could shift quickly in a direction you don't want. Point to relevant sections of your community guidelines. [Read more about defining and discouraging this kind of behavior here.](https://guides.coralproject.net/manage-a-successful-community/) +* If people are sharing links to conspiracy sites or other unreliable sources, consider setting either the article (via the Configure tab on the article page) or the whole site (via Configure in the Admin view) to Pre-moderate Links, and delete any comments that link to sites that exceed your guidelines.![An image showing the Pre-moderate Links option in the moderation console](http://blog.coralproject.net/wp-content/uploads/2018/03/premodlinks-site.jpg) ---------------------- ![A screenshot of the Configure tab in the comments stream with the Pre-Moderate Links option circled in pink](http://blog.coralproject.net/wp-content/uploads/2018/03/premodllinks-article.jpg) + +* If the conversation is getting out of hand, set the article to Pre-moderation (via the Configure tab on the comments, see above) and tell your community members you've done it, via Ask a Question (see above). If the conversation doesn't improve, or you feel that it is beyond redemption, consider closing the article to comments altogether via the Configure tab or the Stories tab in the moderation view, and telling the community why you have done so. +* Make sure that your community has an opportunity to give feedback and shape your guidelines. Create a page on your site for meta-discussions about your policies – this could either be one static page, or an ongoing series of updates (see below). Encourage your community to interrogate your standards, and participate in improving them. This will help give your community a sense of co-ownership over the space, and encourage them to help enforce its codes. + +#### Consistently + +* Create [a clear series of guidelines](https://guides.coralproject.net/create-a-code-of-conduct/) that your community members can easily reference, and [set up your moderation strategy ahead of time.](https://guides.coralproject.net/how-to-moderate-effectively/) +* When you ban/suspend a community member, include language in the email they are sent through Talk that explains which aspect/s of your guidelines they have ignored. +* Make sure that any new member of your moderation team receives training before they begin, that you make sure that everyone in your moderation team is watching each other [for signs of secondary trauma](https://www.counseling.org/docs/trauma-disaster/fact-sheet-9---vicarious-trauma.pdf), and that everyone knows they can step away at any time if things get difficult. [Here's a piece on how to create an effective moderation team.](https://guides.coralproject.net/creating-a-successful-community-management-team/) [You can read more about the emotional labor of moderation here.](https://guides.coralproject.net/supporting-emotional-labor-in-moderation/) + +### 5\. Get journalists on your side + +Most journalists don't like comments. [Also, most journalists read comments.](https://mediaengagement.org/research/journalists-and-online-comments/) If your goal is to bring community closer to your journalism, you need to change the minds of people in your newsroom about the value of your onsite community. There are two reasons to do this: to improve the community, and to improve the journalism. **For the community:** [As a study from the Center for Media Engagement (CME) shows](https://mediaengagement.org/research/journalist-involvement/), comments are more civil when a journalist engages in the space. [A separate study that we commissioned from the CME](https://mediaengagement.org/research/comment-section-survey-across-20-news-sites/) demonstrates that the majority of commenters across sites of all sizes want journalists to engage in the comments. **For the journalism:** [there is real potential value in the comments](https://guides.coralproject.net/why-community-work-is-important/), in helping journalists find tips and sources, in finding important clarifications and corrections, in building a loyal audience, and in involving your community in your mission. These are some of your most dedicated readers. They deserve your attention. That said, journalists need to be prepared for how to engage effectively. [As this guide on engaging in the comments](http://niemanreports.org/articles/getting-the-most-out-of-comments-a-guide-for-journalists/) states, the main principles for how to act in the comments should be **Thank**, **Engage** (through Featured comments, replying to the commenter), **Share** (via social media and, where appropriate, in follow-up articles.) If you are going to ask journalists moderate comments, we recommend that they don't moderate their own but instead pair with someone else to moderate each other's, before the author of the piece goes into the comments to engage. In that way, the worst abuse and criticism can be removed by someone who is less likely to take it personally. You should also instruct journalists on how to escalate potentially credible threats to a senior editor, and make it clear that it's ok to step away if the task starts to affect them personally. [There are more tips on supporting the emotional health of people who moderate comments here.](https://guides.coralproject.net/supporting-emotional-labor-in-moderation/) + +### 6\. Launch with care + +We strongly recommend launching Talk on just one article, talking about the change that will be coming to the rest of the site, describing the features, and letting your community kick the tires on the new system before it is released everywhere. Doing this will allow your community members to get used to the change, to make suggestions, to enter into conversation with you about the switch, and to help make them feel included and less surprised about the change when it goes sitewide. It will also allow you to get used to the moderation interface without being overwhelmed. [Here's how The Washington Post used their community to test the new system.](https://www.washingtonpost.com/news/ask-the-post/2017/06/15/everybody-talk-round-2-of-testing-for-the-coral-projects-comment-software/?utm_term=.10f68bbb671a) If you can't release Talk in this way, we recommend that you announce the launch of the new system in a standalone article, describing the features in Talk (especially 'Ignore User', My Profile, Notifications, and Report functions. Screenshots with arrows can help - if you're a Mac user, try [Skitch](https://evernote.com/products/skitch)), explaining why you've moved to Talk, describing the benefits Talk brings, and the changes you will be looking for/promises you will make to the community moving forward. You should take more time than usual to guide people, answer questions, and collect suggestions for improvements (we'd love to hear them.) [Here's how The Intercept did this.](https://theintercept.com/2017/12/18/comments-coral-project/) For the first month or so, we recommend including a link to the launch article mentioned above in the Comments Stream Description box on every Talk page. This will give community members a place to go to discuss the new system, so that they are more likely to be on topic on the articles themselves. We continue to add more features to Talk every few weeks. For significant feature changes, we suggest writing a follow-up article to introduce them to your community, pointing out the features and encouraging more feedback on the system itself, and on how you are managing the community. A regular space for conversation about the conversation is always welcome in any successful community, and is a great source of ideas for improvement.   **_We hope you enjoy using Talk, and that it helps your communities to thrive. If you have any questions or suggestions for this piece, or would like to try Talk on your site, please [contact us.](https://coralproject.net/contact.html)_**   [_Red button image by włodi_](https://www.flickr.com/photos/wlodi/3085157011/)_, CC-BY-SA 2.0_ From f69163d9c24e5879b80fe6b07b816a077895d791 Mon Sep 17 00:00:00 2001 From: Mendel Konikov Date: Fri, 23 Mar 2018 09:21:31 -0400 Subject: [PATCH 14/21] Clarifying importance of order in plugins.json --- docs/source/api/server.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/api/server.md b/docs/source/api/server.md index 614c7b2ed..d25487c2d 100644 --- a/docs/source/api/server.md +++ b/docs/source/api/server.md @@ -377,7 +377,11 @@ en: Which overrides the copy for the `embedlink.copy` template. You can also provide other languages as well by using the correct language -prefix. +prefix. + +When creating a plugin using this `translations` hook to override copy +from another plugin, be sure to list it after the plugin it's overriding +in the `plugins.json` file. ### websockets @@ -524,4 +528,4 @@ module.exports = { } }; -``` \ No newline at end of file +``` From d7cf733a4298a885b20c936ecdc115d4a9f9f845 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 23 Mar 2018 11:15:47 -0600 Subject: [PATCH 15/21] Moved loadable to the markdown editor --- .../{loadable => }/QuestionBoxBuilder.css | 0 .../components/QuestionBoxBuilder.js | 61 ++++++- .../components/loadable/QuestionBoxBuilder.js | 56 ------ .../components/MarkdownEditor.js | 161 +----------------- .../{ => loadable}/MarkdownEditor.css | 0 .../components/loadable/MarkdownEditor.js | 154 +++++++++++++++++ 6 files changed, 216 insertions(+), 216 deletions(-) rename client/coral-embed-stream/src/tabs/configure/components/{loadable => }/QuestionBoxBuilder.css (100%) delete mode 100644 client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js rename client/coral-framework/components/{ => loadable}/MarkdownEditor.css (100%) create mode 100644 client/coral-framework/components/loadable/MarkdownEditor.js diff --git a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css similarity index 100% rename from client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css rename to client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css 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 e87e7b321..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,11 +1,56 @@ -import { Spinner } from 'coral-ui'; -import Loadable from 'react-loadable'; +import React from 'react'; +import QuestionBox from '../../../components/QuestionBox'; +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 QuestionBoxBuilder = Loadable({ - loader: () => - import(/* webpackChunkName: "questionBoxBuilder" */ - './loadable/QuestionBoxBuilder'), - loading: Spinner, -}); +const DefaultIcon = ; +const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote']; + +class QuestionBoxBuilder extends React.Component { + render() { + const { + questionBoxIcon, + questionBoxContent, + onContentChange, + onIconChange, + } = this.props; + + return ( +
+

Include an Icon

+ +
    + {icons.map(item => { + const name = typeof item === 'object' ? Object.keys(item)[0] : item; + const icon = typeof item === 'object' ? item[name] : item; + return ( +
  • + +
  • + ); + })} +
+ + + + +
+ ); + } +} export default QuestionBoxBuilder; diff --git a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js deleted file mode 100644 index 837de1280..000000000 --- a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import QuestionBox from '../../../../components/QuestionBox'; -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 { - render() { - const { - questionBoxIcon, - questionBoxContent, - onContentChange, - onIconChange, - } = this.props; - - return ( -
-

Include an Icon

- -
    - {icons.map(item => { - const name = typeof item === 'object' ? Object.keys(item)[0] : item; - const icon = typeof item === 'object' ? item[name] : item; - return ( -
  • - -
  • - ); - })} -
- - - - -
- ); - } -} - -export default QuestionBoxBuilder; diff --git a/client/coral-framework/components/MarkdownEditor.js b/client/coral-framework/components/MarkdownEditor.js index e9658ec69..ce92a2437 100644 --- a/client/coral-framework/components/MarkdownEditor.js +++ b/client/coral-framework/components/MarkdownEditor.js @@ -1,154 +1,11 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import SimpleMDE from 'simplemde'; -import cn from 'classnames'; -import noop from 'lodash/noop'; -import styles from './MarkdownEditor.css'; +import { Spinner } from 'coral-ui'; +import Loadable from 'react-loadable'; -const config = { - status: false, +const MarkdownEditor = Loadable({ + loader: () => + import(/* webpackChunkName: "markdownEditor" */ + './loadable/MarkdownEditor'), + loading: Spinner, +}); - // Do not download fontAwesome icons as we replace them with - // material icons. - autoDownloadFontAwesome: false, - - // Disable built-in spell checker as it is very rudimentary. - spellChecker: false, - - toolbar: [ - { - name: 'bold', - action: SimpleMDE.toggleBold, - className: styles.iconBold, - title: 'Bold', - }, - { - name: 'italic', - action: SimpleMDE.toggleItalic, - className: styles.iconItalic, - title: 'Italic', - }, - { - name: 'title', - action: SimpleMDE.toggleHeadingSmaller, - className: styles.iconTitle, - title: 'Title, Subtitle, Heading', - }, - '|', - { - name: 'quote', - action: SimpleMDE.toggleBlockquote, - className: styles.iconQuote, - title: 'Quote', - }, - { - name: 'unordered-list', - action: SimpleMDE.toggleUnorderedList, - className: styles.iconUnorderedList, - title: 'Generic List', - }, - { - name: 'ordered-list', - action: SimpleMDE.toggleOrderedList, - className: styles.iconOrderedList, - title: 'Numbered List', - }, - '|', - { - name: 'link', - action: SimpleMDE.drawLink, - className: styles.iconLink, - title: 'Create Link', - }, - { - name: 'image', - action: SimpleMDE.drawImage, - className: styles.iconImage, - title: 'Insert Image', - }, - '|', - { - name: 'preview', - action: SimpleMDE.togglePreview, - className: cn(styles.iconPreview, 'no-disable'), - title: 'Toggle Preview', - }, - { - name: 'side-by-side', - action: SimpleMDE.toggleSideBySide, - className: cn(styles.iconSideBySide, 'no-disable'), - title: 'Toggle Side by Side', - }, - { - name: 'fullscreen', - action: SimpleMDE.toggleFullScreen, - className: cn(styles.iconFullscreen, 'no-disable'), - title: 'Toggle Fullscreen', - }, - '|', - { - name: 'guide', - action: 'https://simplemde.com/markdown-guide', - className: styles.iconGuide, - title: 'Markdown Guide', - }, - ], -}; - -export default class MarkdownEditor extends Component { - textarea = null; - editor = null; - - onRef = ref => (this.textarea = ref); - - componentDidMount() { - this.editor = new SimpleMDE({ - ...config, - element: this.textarea, - }); - - // Don't trap the key, to stay accessible. - this.editor.codemirror.options.extraKeys['Tab'] = false; - this.editor.codemirror.options.extraKeys['Shift-Tab'] = false; - - this.editor.codemirror.on('change', this.onChange); - } - - componentWillReceiveProps(nextProps) { - if ( - this.props.value !== nextProps.value && - nextProps.value !== this.editor.value() - ) { - this.editor.value(nextProps.value); - } - } - - componentDidUpdate() { - // Workaround empty render issue. - // https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313 - this.editor.codemirror.refresh(); - } - - componentWillUnmount() { - this.editor.toTextArea(); - } - - onChange = () => { - if (this.props.onChange) { - this.props.onChange(this.editor.value()); - } - }; - - render() { - return ( -
-