diff --git a/.gitignore b/.gitignore
index 7c0007dc9..6f3ac70b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,7 +62,6 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-rich-text
-!plugins/talk-plugin-rich-text-pell
**/node_modules/*
yarn-error.log
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-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/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
index 191490914..9687a9370 100644
--- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
@@ -465,7 +465,6 @@ const mapStateToProps = state => ({
activeStreamTab: state.stream.activeTab,
previousStreamTab: state.stream.previousTab,
commentClassNames: state.stream.commentClassNames,
- pluginConfig: state.config.plugin_config,
sortOrder: state.stream.sortOrder,
sortBy: state.stream.sortBy,
});
diff --git a/client/coral-embed/src/Stream.js b/client/coral-embed/src/Stream.js
index fc8c0d5e7..d0ada6080 100644
--- a/client/coral-embed/src/Stream.js
+++ b/client/coral-embed/src/Stream.js
@@ -161,6 +161,14 @@ export default class Stream {
);
}
+ enablePluginsDebug() {
+ this.pym.sendMessage('enablePluginsDebug');
+ }
+
+ disablePluginsDebug() {
+ this.pym.sendMessage('disablePluginsDebug');
+ }
+
login(token) {
this.pym.sendMessage('login', token);
}
diff --git a/client/coral-embed/src/StreamInterface.js b/client/coral-embed/src/StreamInterface.js
index 4c6e29970..1e54dd155 100644
--- a/client/coral-embed/src/StreamInterface.js
+++ b/client/coral-embed/src/StreamInterface.js
@@ -7,6 +7,10 @@ export default class StreamInterface {
return this._stream.emitter.on(eventName, callback);
}
+ off(eventName, callback) {
+ return this._stream.emitter.off(eventName, callback);
+ }
+
login(token) {
return this._stream.login(token);
}
@@ -18,4 +22,12 @@ export default class StreamInterface {
remove() {
return this._stream.remove();
}
+
+ enablePluginsDebug() {
+ return this._stream.enablePluginsDebug();
+ }
+
+ disablePluginsDebug() {
+ return this._stream.disablePluginsDebug();
+ }
}
diff --git a/client/coral-framework/actions/config.js b/client/coral-framework/actions/config.js
index dd1522333..859fac4d5 100644
--- a/client/coral-framework/actions/config.js
+++ b/client/coral-framework/actions/config.js
@@ -1,6 +1,18 @@
-import { MERGE_CONFIG } from '../constants/config';
+import {
+ MERGE_CONFIG,
+ ENABLE_PLUGINS_DEBUG,
+ DISABLE_PLUGINS_DEBUG,
+} from '../constants/config';
export const mergeConfig = config => ({
type: MERGE_CONFIG,
config,
});
+
+export const enablePluginsDebug = () => ({
+ type: ENABLE_PLUGINS_DEBUG,
+});
+
+export const disablePluginsDebug = () => ({
+ type: DISABLE_PLUGINS_DEBUG,
+});
diff --git a/client/coral-framework/components/Slot.css b/client/coral-framework/components/Slot.css
index 6a95d79ad..69bfe937e 100644
--- a/client/coral-framework/components/Slot.css
+++ b/client/coral-framework/components/Slot.css
@@ -3,5 +3,36 @@
}
.debug {
- background-color: coral;
+ background-color: #e2e2e2;
+ border-style: dotted solid;
+ border-width: 2px;
+ border: dotted 2px coral;
+ padding: 2px;
+ margin: 1px;
+ position: relative;
+}
+
+.debug::before {
+ content: attr(data-slot-name);
+ display: inline-block;
+ position: absolute;
+
+ background: #000;
+ color: #FFF;
+ padding: 5px;
+ border-radius: 5px;
+ opacity: 0;
+ transition: 0.3s;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: 999!important;
+ white-space: pre-wrap;
+ min-height: 16px;
+ top: 50%;
+ left: 0;
+}
+
+.debug:hover::before {
+ opacity: 1;
+ top: 100%;
}
\ No newline at end of file
diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js
index a2f90161f..048c4da80 100644
--- a/client/coral-framework/components/Slot.js
+++ b/client/coral-framework/components/Slot.js
@@ -30,6 +30,7 @@ class Slot extends React.Component {
className,
`talk-slot-${kebabCase(fill)}`
)}
+ data-slot-name={fill}
>
{children}
diff --git a/client/coral-framework/constants/config.js b/client/coral-framework/constants/config.js
index bf846af1d..b18b67387 100644
--- a/client/coral-framework/constants/config.js
+++ b/client/coral-framework/constants/config.js
@@ -1,3 +1,5 @@
const prefix = `TALK_FRAMEWORK`;
export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`;
+export const ENABLE_PLUGINS_DEBUG = `${prefix}_ENABLE_PLUGINS_DEBUG`;
+export const DISABLE_PLUGINS_DEBUG = `${prefix}_DISABLE_PLUGINS_DEBUG`;
diff --git a/client/coral-framework/hocs/withSlotElements.js b/client/coral-framework/hocs/withSlotElements.js
index a4b39c907..9b432ade8 100644
--- a/client/coral-framework/hocs/withSlotElements.js
+++ b/client/coral-framework/hocs/withSlotElements.js
@@ -83,6 +83,13 @@ const createHOC = ({
}
if (changes.length === 1 && changes[0] === 'reduxState') {
+ // If config changed, we'll have to rerender everything.
+ // Should only happen during development as this is
+ // usually static.
+ if (this.props.reduxState.config !== next.reduxState.config) {
+ return true;
+ }
+
const prevChildrenKeys = this.getSlotElements(this.props).map(
child => child.key
);
diff --git a/client/coral-framework/reducers/config.js b/client/coral-framework/reducers/config.js
index f7aebd9e2..b063f9b97 100644
--- a/client/coral-framework/reducers/config.js
+++ b/client/coral-framework/reducers/config.js
@@ -1,10 +1,30 @@
-import { MERGE_CONFIG } from '../constants/config';
+import {
+ MERGE_CONFIG,
+ ENABLE_PLUGINS_DEBUG,
+ DISABLE_PLUGINS_DEBUG,
+} from '../constants/config';
import { LOGOUT } from '../constants/auth';
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
+ case ENABLE_PLUGINS_DEBUG:
+ return {
+ ...state,
+ plugins_config: {
+ ...state.plugins_config,
+ debug: true,
+ },
+ };
+ case DISABLE_PLUGINS_DEBUG:
+ return {
+ ...state,
+ plugins_config: {
+ ...state.plugins_config,
+ debug: false,
+ },
+ };
case LOGOUT:
return {
...state,
diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js
index 691404369..e0bfbb950 100644
--- a/client/coral-framework/services/bootstrap.js
+++ b/client/coral-framework/services/bootstrap.js
@@ -25,7 +25,11 @@ import { createIntrospection } from 'coral-framework/services/introspection';
import introspectionData from 'coral-framework/graphql/introspection.json';
import coreReducers from '../reducers';
import { checkLogin as checkLoginAction } from '../actions/auth';
-import { mergeConfig } from '../actions/config';
+import {
+ mergeConfig,
+ enablePluginsDebug,
+ disablePluginsDebug,
+} from '../actions/config';
import { setAuthToken, logout } from '../actions/auth';
/**
@@ -62,8 +66,19 @@ function initExternalConfig({ store, pym, inIframe }) {
}
return new Promise(resolve => {
pym.sendMessage('getConfig');
- pym.onMessage('config', config => {
- store.dispatch(mergeConfig(JSON.parse(config)));
+ pym.onMessage('config', rawConfig => {
+ const config = JSON.parse(rawConfig);
+ if (config.plugin_config) {
+ // @Deprecated
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'Deprecation Warning: `config.plugin_config` will be phased out soon, please replace `config.plugin_config with `config.plugins_config`'
+ );
+ }
+ config.plugins_config = config.plugin_config;
+ delete config.plugin_config;
+ }
+ store.dispatch(mergeConfig(config));
resolve();
});
});
@@ -215,6 +230,14 @@ export async function createContext({
pym.onMessage('logout', () => {
store.dispatch(logout());
});
+
+ pym.onMessage('enablePluginsDebug', () => {
+ store.dispatch(enablePluginsDebug());
+ });
+
+ pym.onMessage('disablePluginsDebug', () => {
+ store.dispatch(disablePluginsDebug());
+ });
}
const preInitList = [];
diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js
index 4c09c1047..bb16fc5e7 100644
--- a/client/coral-framework/services/plugins.js
+++ b/client/coral-framework/services/plugins.js
@@ -11,7 +11,7 @@ import values from 'lodash/values';
import { getDisplayName } from 'coral-framework/helpers/hoc';
import camelize from '../helpers/camelize';
-// This is returned for pluginConfig when it is empty.
+// This is returned for pluginsConfig when it is empty.
const emptyConfig = {};
// Memoize the warnings so we only show them once.
@@ -73,10 +73,10 @@ function addMetaDataToSlotComponents(plugins) {
* query datas are only passed to the component if it is defined in `component.fragments`.
*/
function getSlotComponentProps(component, reduxState, props, queryData) {
- const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
+ const pluginsConfig = get(reduxState, 'config.plugins_config') || emptyConfig;
return {
...props,
- config: pluginConfig,
+ config: pluginsConfig,
...(component.fragments
? pick(queryData, Object.keys(component.fragments))
: withWarnings(component, queryData)),
@@ -125,15 +125,16 @@ class PluginsService {
* Returns React Elements for given slot.
*/
getSlotElements(slot, reduxState, props = {}, options = {}) {
- const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
+ const pluginsConfig =
+ get(reduxState, 'config.plugins_config') || emptyConfig;
const { size = 0 } = options;
const { queryData, rest } = splitProps(props);
const isDisabled = component => {
if (
- pluginConfig &&
- pluginConfig[component.talkPluginName] &&
- pluginConfig[component.talkPluginName].disable_components
+ pluginsConfig &&
+ pluginsConfig[component.talkPluginName] &&
+ pluginsConfig[component.talkPluginName].disable_components
) {
return true;
}
diff --git a/docs/_config.yml b/docs/_config.yml
index 2a6fa2725..f524fed0a 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -136,6 +136,8 @@ sidebar:
url: /plugins-directory/
- title: Plugin Recipes
url: /plugin-recipes/
+ - title: Slots and Plugins
+ url: /slots-and-plugins/
- title: Tutorials
children:
- title: Creating a Basic Plugin
diff --git a/docs/source/04-06-slots-and-plugins.md b/docs/source/04-06-slots-and-plugins.md
new file mode 100644
index 000000000..907c85862
--- /dev/null
+++ b/docs/source/04-06-slots-and-plugins.md
@@ -0,0 +1,169 @@
+---
+title: Slots and Plugins
+permalink: /slots-and-plugins/
+---
+
+Plugins make use of **"slots"** in order to change Talk's interface.
+
+By default, Talk has various plugins provided by default. We can see this in `plugins.default.json`:
+
+```json
+{
+ "server": [
+ "talk-plugin-auth",
+ "talk-plugin-featured-comments",
+ "talk-plugin-offtopic",
+ "talk-plugin-respect"
+ ],
+ "client": [
+ "talk-plugin-auth",
+ "talk-plugin-author-menu",
+ "talk-plugin-comment-content",
+ "talk-plugin-featured-comments",
+ "talk-plugin-flag-details",
+ "talk-plugin-ignore-user",
+ "talk-plugin-member-since",
+ "talk-plugin-moderation-actions",
+ "talk-plugin-offtopic",
+ "talk-plugin-permalink",
+ "talk-plugin-respect",
+ "talk-plugin-sort-most-replied",
+ "talk-plugin-sort-most-respected",
+ "talk-plugin-sort-newest",
+ "talk-plugin-sort-oldest",
+ "talk-plugin-viewing-options",
+ "talk-plugin-profile-settings"
+ ]
+}
+```
+
+Let's only focus on the plugins which are listed under `client` - these are the plugins that use *slots* to inject certain functionality into the Talk UI.
+
+For example, if we look at the Respect plugin (`talk-plugin-respect`), we can see its `client/index.js` looks like this:
+
+
+```js
+import RespectButton from './RespectButton';
+import translations from './translations.yml';
+
+export default {
+ translations,
+ slots: {
+ commentReactions: [RespectButton],
+ },
+};
+
+```
+
+Inside the `slots` property, we specify which **slots** the plugin will use. Above we are saying that the `RespectButton` component is being injected into the slot `commentReactions`.
+
+Slots can receive an Array of components, so we can use one plugin or many for one slot.
+
+### Anatomy of the Slot Component
+
+In Talk core, we have 32 slots available for us to use. The component `Slot` has a `fill` property where we establish the name of the slot. It looks like this:
+
+
+```js
+
/g, '
instead of
instead of