diff --git a/.dockerignore b/.dockerignore
index e413ac5ea..80bb20e2c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,9 +1,10 @@
# excluded because we'll likely need to rebuild this.
node_modules
-# scripts are used during development and testing, not
+# most scripts are used during development and testing, not
# production.
scripts
+!scripts/generateIntrospectionResult.js
# documentation should not be visable in production.
docs
diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js
index 382f5101d..14437c51b 100644
--- a/client/coral-admin/src/index.js
+++ b/client/coral-admin/src/index.js
@@ -2,6 +2,8 @@ import React from 'react';
import {render} from 'react-dom';
import {ApolloProvider} from 'react-apollo';
import smoothscroll from 'smoothscroll-polyfill';
+import EventEmitter from 'eventemitter2';
+import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
import {getClient} from 'coral-framework/services/client';
import store from './services/store';
@@ -12,13 +14,19 @@ import 'react-mdl/extra/material.js';
import './graphql';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
+const eventEmitter = new EventEmitter();
+
+// TODO: pass redux actions through the emitter.
+
loadPluginsTranslations();
injectPluginsReducers();
smoothscroll.polyfill();
render(
-
-
-
+
+
+
+
+
, document.querySelector('#root')
);
diff --git a/client/coral-embed-stream/src/components/Stream.css b/client/coral-embed-stream/src/components/Stream.css
index 8c5a4250b..23f620157 100644
--- a/client/coral-embed-stream/src/components/Stream.css
+++ b/client/coral-embed-stream/src/components/Stream.css
@@ -14,7 +14,7 @@
.filterWrapper {
position: absolute;
right: 0;
- margin-top: 6px;
+ margin-top: 4px;
}
.highlightedContainer {
@@ -26,4 +26,4 @@
margin-top: 28px;
padding-bottom: 50px;
min-height: 600px;
-}
+}
\ No newline at end of file
diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js
index 9020a8457..c40df74f1 100644
--- a/client/coral-embed-stream/src/index.js
+++ b/client/coral-embed-stream/src/index.js
@@ -5,15 +5,19 @@ import {ApolloProvider} from 'react-apollo';
import {checkLogin} from 'coral-framework/actions/auth';
import './graphql';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
-import {getStore, injectReducers} from 'coral-framework/services/store';
+import {getStore, injectReducers, addListener} from 'coral-framework/services/store';
import {getClient} from 'coral-framework/services/client';
+import {createReduxEmitter} from 'coral-framework/services/events';
import pym from 'coral-framework/services/pym';
import AppRouter from './AppRouter';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
import reducers from './reducers';
+import EventEmitter from 'eventemitter2';
+import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
const store = getStore();
const client = getClient();
+const eventEmitter = new EventEmitter({wildcard: true});
loadPluginsTranslations();
injectPluginsReducers();
@@ -42,11 +46,21 @@ if (!window.opener) {
} else {
init();
}
+
+ // Pass any events through our parent.
+ eventEmitter.onAny((eventName, value) => {
+ pym.sendMessage('event', JSON.stringify({eventName, value}));
+ });
+
+ // Add a redux listener to pass through all actions to our event emitter.
+ addListener(createReduxEmitter(eventEmitter));
}
render(
-
-
-
+
+
+
+
+
, document.querySelector('#talk-embed-stream-container')
);
diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js
index eb61f3b90..fda97c4b3 100644
--- a/client/coral-embed/src/index.js
+++ b/client/coral-embed/src/index.js
@@ -3,6 +3,9 @@ import URLSearchParams from 'url-search-params';
import {buildUrl} from 'coral-framework/utils';
import queryString from 'query-string';
+import EventEmitter from 'eventemitter2';
+
+const eventEmitter = new EventEmitter({wildcard: true});
// TODO: Styles should live in a separate file
const snackbarStyles = {
@@ -57,6 +60,10 @@ function configurePymParent(pymParent, opts) {
pymParent.sendMessage('config', JSON.stringify(config));
}
+ if (opts.events) {
+ opts.events(eventEmitter);
+ }
+
pymParent.onMessage('coral-auth-changed', function(message) {
if (opts.onAuthChanged) {
opts.onAuthChanged(message ? JSON.parse(message) : null);
@@ -167,6 +174,12 @@ function configurePymParent(pymParent, opts) {
window.open(url, '_blank').focus();
});
+ // Pass events from iframe to the event emitter
+ pymParent.onMessage('event', (raw) => {
+ const {eventName, value} = JSON.parse(raw);
+ eventEmitter.emit(eventName, value);
+ });
+
// get dimensions of viewport
const viewport = () => {
let e = window, a = 'inner';
diff --git a/client/coral-framework/components/EventEmitterProvider.js b/client/coral-framework/components/EventEmitterProvider.js
new file mode 100644
index 000000000..36c0bccdb
--- /dev/null
+++ b/client/coral-framework/components/EventEmitterProvider.js
@@ -0,0 +1,18 @@
+import React from 'react';
+const PropTypes = require('prop-types');
+
+class EventEmitterProvider extends React.Component {
+ getChildContext() {
+ return {eventEmitter: this.props.eventEmitter};
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+EventEmitterProvider.childContextTypes = {
+ eventEmitter: PropTypes.object,
+};
+
+export default EventEmitterProvider;
diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index 782f840bd..67b47482e 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -2,3 +2,6 @@ export {default as withFragments} from './withFragments';
export {default as withMutation} from './withMutation';
export {default as withQuery} from './withQuery';
export {default as withCopyToClipboard} from './withCopyToClipboard';
+export {default as withEmit} from './withEmit';
+export {default as excludeIf} from './excludeIf';
+export {default as connect} from './connect';
diff --git a/client/coral-framework/hocs/withEmit.js b/client/coral-framework/hocs/withEmit.js
new file mode 100644
index 000000000..3a3216c8a
--- /dev/null
+++ b/client/coral-framework/hocs/withEmit.js
@@ -0,0 +1,27 @@
+import React from 'react';
+const PropTypes = require('prop-types');
+
+/**
+ * WithEmit provides a property `emit: (eventName, value)`
+ * to the wrapped component.
+ */
+export default (WrappedComponent) => {
+ class WithEmit extends React.Component {
+ static contextTypes = {
+ eventEmitter: PropTypes.object,
+ };
+
+ emit = (eventName, value) => {
+ this.context.eventEmitter.emit(eventName, value);
+ };
+
+ render() {
+ return ;
+ }
+ }
+
+ return WithEmit;
+};
diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js
index d3ba73ecd..4488b0e7f 100644
--- a/client/coral-framework/hocs/withMutation.js
+++ b/client/coral-framework/hocs/withMutation.js
@@ -5,8 +5,8 @@ import uniq from 'lodash/uniq';
import flatten from 'lodash/flatten';
import isEmpty from 'lodash/isEmpty';
import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
-import {getStore} from 'coral-framework/services/store';
import {getDefinitionName, getResponseErrors} from '../utils';
+import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
class ResponseErrors extends Error {
@@ -36,98 +36,115 @@ export default (document, config = {}) => (WrappedComponent) => {
options: config.options || {},
props: config.props || ((data) => ({mutate: data.mutate()})),
};
- const wrappedProps = (data) => {
- const name = getDefinitionName(document);
- const callbacks = getMutationOptions(name);
- const mutate = (base) => {
- const variables = base.variables || config.options.variables;
- const configs = callbacks.map((cb) => cb({variables, state: getStore().getState()}));
- const optimisticResponse = merge(
- base.optimisticResponse || config.options.optimisticResponse,
- ...configs.map((cfg) => cfg.optimisticResponse),
- );
-
- const refetchQueries = flatten(uniq([
- base.refetchQueries || config.options.refetchQueries,
- ...configs.map((cfg) => cfg.refetchQueries),
- ].filter((i) => i)));
-
- const updateCallbacks =
- [base.update || config.options.update]
- .concat(...configs.map((cfg) => cfg.update))
- .filter((i) => i);
-
- const update = (proxy, result) => {
- if (getResponseErrors(result)) {
-
- // Do not run updates when we have mutation errors.
- return;
- }
- updateCallbacks.forEach((cb) => cb(proxy, result));
- };
-
- const updateQueries =
- [
- base.updateQueries || config.options.updateQueries,
- ...configs.map((cfg) => cfg.updateQueries)
- ]
- .filter((i) => i)
- .reduce((res, map) => {
- Object.keys(map).forEach((key) => {
- if (!(key in res)) {
- res[key] = (prev, result) => {
- if (getResponseErrors(result.mutationResult)) {
-
- // Do not run updates when we have mutation errors.
- return prev;
- }
- return map[key](prev, result);
- };
- } else {
- const existing = res[key];
- res[key] = (prev, result) => {
- const next = existing(prev, result);
- return map[key](next, result);
- };
- }
- });
- return res;
- }, {});
-
- const wrappedConfig = {
- variables,
- optimisticResponse,
- refetchQueries,
- updateQueries,
- update,
- };
- if (isEmpty(wrappedConfig.optimisticResponse)) {
- delete wrappedConfig.optimisticResponse;
- }
- return data.mutate(wrappedConfig)
- .then((res) => {
- const errors = getResponseErrors(res);
- if (errors) {
- throw new ResponseErrors(errors);
- }
- return Promise.resolve(res);
- });
+ return class WithMutation extends React.Component {
+ static contextTypes = {
+ eventEmitter: PropTypes.object,
+ store: PropTypes.object,
};
- return config.props({...data, mutate});
- };
- // Lazily resolve fragments from graphRegistry to support circular dependencies.
- let memoized = null;
- const getWrapped = () => {
- if (!memoized) {
- memoized = graphql(resolveFragments(document), {...config, props: wrappedProps})(WrappedComponent);
+ // Lazily resolve fragments from graphRegistry to support circular dependencies.
+ memoized = null;
+
+ wrappedProps = (data) => {
+ const name = getDefinitionName(document);
+ const callbacks = getMutationOptions(name);
+ const mutate = (base) => {
+ const variables = base.variables || config.options.variables;
+ const configs = callbacks.map((cb) => cb({variables, state: this.context.store.getState()}));
+
+ const optimisticResponse = merge(
+ base.optimisticResponse || config.options.optimisticResponse,
+ ...configs.map((cfg) => cfg.optimisticResponse),
+ );
+
+ const refetchQueries = flatten(uniq([
+ base.refetchQueries || config.options.refetchQueries,
+ ...configs.map((cfg) => cfg.refetchQueries),
+ ].filter((i) => i)));
+
+ const updateCallbacks =
+ [base.update || config.options.update]
+ .concat(...configs.map((cfg) => cfg.update))
+ .filter((i) => i);
+
+ const update = (proxy, result) => {
+ if (getResponseErrors(result)) {
+
+ // Do not run updates when we have mutation errors.
+ return;
+ }
+ updateCallbacks.forEach((cb) => cb(proxy, result));
+ };
+
+ const updateQueries =
+ [
+ base.updateQueries || config.options.updateQueries,
+ ...configs.map((cfg) => cfg.updateQueries)
+ ]
+ .filter((i) => i)
+ .reduce((res, map) => {
+ Object.keys(map).forEach((key) => {
+ if (!(key in res)) {
+ res[key] = (prev, result) => {
+ if (getResponseErrors(result.mutationResult)) {
+
+ // Do not run updates when we have mutation errors.
+ return prev;
+ }
+ return map[key](prev, result);
+ };
+ } else {
+ const existing = res[key];
+ res[key] = (prev, result) => {
+ const next = existing(prev, result);
+ return map[key](next, result);
+ };
+ }
+ });
+ return res;
+ }, {});
+
+ const wrappedConfig = {
+ variables,
+ optimisticResponse,
+ refetchQueries,
+ updateQueries,
+ update,
+ };
+ if (isEmpty(wrappedConfig.optimisticResponse)) {
+ delete wrappedConfig.optimisticResponse;
+ }
+
+ this.context.eventEmitter.emit(`mutation.${name}.begin`, {variables});
+
+ return data.mutate(wrappedConfig)
+ .then((res) => {
+ const errors = getResponseErrors(res);
+ if (errors) {
+ throw new ResponseErrors(errors);
+ }
+ this.context.eventEmitter.emit(`mutation.${name}.success`, {variables, data: res.data});
+ return Promise.resolve(res);
+ })
+ .catch((error) => {
+ this.context.eventEmitter.emit(`mutation.${name}.error`, {variables, error});
+ throw new error;
+ });
+ };
+ return config.props({...data, mutate});
+ };
+
+ getWrapped = () => {
+ if (!this.memoized) {
+ this.memoized = graphql(resolveFragments(document), {...config, props: this.wrappedProps})(WrappedComponent);
+ }
+ return this.memoized;
+ };
+
+ render() {
+ const Wrapped = this.getWrapped();
+ return ;
}
- return memoized;
- };
-
- return (props) => {
- const Wrapped = getWrapped();
- return ;
};
};
diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js
index ec190466c..adb67b7b3 100644
--- a/client/coral-framework/hocs/withQuery.js
+++ b/client/coral-framework/hocs/withQuery.js
@@ -2,6 +2,7 @@ import * as React from 'react';
import {graphql} from 'react-apollo';
import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils';
+import PropTypes from 'prop-types';
const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) {
@@ -10,76 +11,146 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
return reducer(prev, action, ...rest);
};
+function networkStatusToString(networkStatus) {
+ switch(networkStatus) {
+ case 1:
+ return 'loading';
+ case 2:
+ return 'setVariables';
+ case 3:
+ return 'fetchMore';
+ case 4:
+ return 'refetch';
+ case 6:
+ return 'poll';
+ case 7:
+ return 'ready';
+ case 8:
+ return 'error';
+ }
+ throw new Error(`Unknown network status ${networkStatus}`);
+}
+
/**
* Exports a HOC with the same signature as `graphql`, that will
* apply query options registered in the graphRegistry.
*/
export default (document, config = {}) => (WrappedComponent) => {
- const wrappedConfig = {
- ...config,
- options: config.options || {},
- props: (args) => {
- const wrappedArgs = {
- ...args,
- data: {
- ...args.data,
- subscribeToMore(stmArgs) {
+ const name = getDefinitionName(document);
- // Resolve document fragments before passing it to `apollo-client`.
- return args.data.subscribeToMore({
- ...stmArgs,
- document: resolveFragments(stmArgs.document),
- });
- },
- fetchMore(lmArgs) {
-
- // Resolve document fragments before passing it to `apollo-client`.
- return args.data.fetchMore({
- ...lmArgs,
- query: resolveFragments(lmArgs.query),
- });
- },
- },
- };
- return config.props
- ? config.props(wrappedArgs)
- : separateDataAndRoot(wrappedArgs.data);
- },
- };
-
- const wrappedOptions = (data) => {
- const base = (typeof wrappedConfig.options === 'function')
- ? wrappedConfig.options(data)
- : wrappedConfig.options;
- const name = getDefinitionName(document);
- const configs = getQueryOptions(name);
- const reducerCallbacks =
- [base.reducer || ((i) => i)]
- .concat(...configs.map((cfg) => cfg.reducer))
- .filter((i) => i);
-
- const reducer = withSkipOnErrors(
- reducerCallbacks.reduce(
- (a, b) => (prev, ...rest) =>
- b(a(prev, ...rest), ...rest),
- ));
-
- return {
- ...base,
- reducer,
+ return class WithQuery extends React.Component {
+ static contextTypes = {
+ eventEmitter: PropTypes.object,
};
- };
- let memoized = null;
- const getWrapped = () => {
- if (!memoized) {
- memoized = graphql(resolveFragments(document), {...wrappedConfig, options: wrappedOptions})(WrappedComponent);
+ // Lazily resolve fragments from graphRegistry to support circular dependencies.
+ memoized = null;
+ lastNetworkStatus = null;
+
+ emitWhenNeeded(data) {
+ const {variables, networkStatus} = data;
+ if (this.lastNetworkStatus === networkStatus) {
+ return;
+ }
+ this.lastNetworkStatus = networkStatus;
+
+ const status = networkStatusToString(networkStatus);
+
+ const {root} = separateDataAndRoot(data);
+ this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, data: root});
}
- return memoized;
- };
- return (props) => {
- const Wrapped = getWrapped();
- return ;
+ wrappedConfig = {
+ ...config,
+ options: config.options || {},
+ props: (args) => {
+ this.emitWhenNeeded(args.data);
+
+ const wrappedArgs = {
+ ...args,
+ data: {
+ ...args.data,
+ subscribeToMore: (stmArgs) => {
+
+ // Resolve document fragments before passing it to `apollo-client`.
+ return args.data.subscribeToMore({
+ ...stmArgs,
+ document: resolveFragments(stmArgs.document),
+ onError: (err) => {
+ if (stmArgs.onErr) {
+ return stmArgs.onErr(err);
+ }
+ throw err;
+ },
+ });
+ },
+ fetchMore: (lmArgs) => {
+ const fetchName = getDefinitionName(lmArgs.query);
+ this.context.eventEmitter.emit(
+ `query.${name}.fetchMore.${fetchName}.begin`,
+ {variables: lmArgs.variables});
+
+ // Resolve document fragments before passing it to `apollo-client`.
+ return args.data.fetchMore({
+ ...lmArgs,
+ query: resolveFragments(lmArgs.query),
+ })
+ .then((res) => {
+ this.context.eventEmitter.emit(
+ `query.${name}.fetchMore.${fetchName}.success`,
+ {variables: lmArgs.variables, data: res.data});
+ return Promise.resolve(res);
+ })
+ .catch((err) => {
+ this.context.eventEmitter.emit(
+ `query.${name}.fetchMore.${fetchName}.error`,
+ {variables: lmArgs.variables, error: err});
+ throw err;
+ });
+ },
+ },
+ };
+ return config.props
+ ? config.props(wrappedArgs)
+ : separateDataAndRoot(wrappedArgs.data);
+ },
+ };
+
+ wrappedOptions = (data) => {
+ const base = (typeof this.wrappedConfig.options === 'function')
+ ? this.wrappedConfig.options(data)
+ : this.wrappedConfig.options;
+ const configs = getQueryOptions(name);
+ const reducerCallbacks =
+ [base.reducer || ((i) => i)]
+ .concat(...configs.map((cfg) => cfg.reducer))
+ .filter((i) => i);
+
+ const reducer = withSkipOnErrors(
+ reducerCallbacks.reduce(
+ (a, b) => (prev, ...rest) =>
+ b(a(prev, ...rest), ...rest),
+ ));
+
+ return {
+ ...base,
+ reducer,
+ };
+ };
+
+ getWrapped = () => {
+ if (!this.memoized) {
+ this.memoized = graphql(
+ resolveFragments(document),
+ {...this.wrappedConfig, options: this.wrappedOptions},
+ )(WrappedComponent);
+ }
+ return this.memoized;
+ };
+
+ render() {
+ const Wrapped = this.getWrapped();
+ return ;
+ }
};
};
diff --git a/client/coral-framework/services/events.js b/client/coral-framework/services/events.js
new file mode 100644
index 000000000..a1a2f4448
--- /dev/null
+++ b/client/coral-framework/services/events.js
@@ -0,0 +1,14 @@
+export function createReduxEmitter(eventEmitter) {
+ return (action) => {
+
+ // Handle apollo actions.
+ if (action.type.startsWith('APOLLO_')) {
+ if (action.type === 'APOLLO_SUBSCRIPTION_RESULT') {
+ const {operationName, variables, result: {data}} = action;
+ eventEmitter.emit(`subscription.${operationName}.data`, {variables, data});
+ }
+ return;
+ }
+ eventEmitter.emit(`action.${action.type}`, {action});
+ };
+}
diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js
index 4d6dafe20..f87a887a3 100644
--- a/client/coral-framework/services/store.js
+++ b/client/coral-framework/services/store.js
@@ -3,12 +3,22 @@ import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {getClient} from './client';
+let listeners = [];
+
export function injectReducers(reducers) {
const store = getStore();
store.coralReducers = {...store.coralReducers, ...reducers};
store.replaceReducer(combineReducers(store.coralReducers));
}
+/**
+ * Add a action listener to the redux store.
+ * The action is passed as the first argument to the callback.
+ */
+export function addListener(cb) {
+ listeners.push(cb);
+}
+
export function getStore() {
if (window.coralStore) {
return window.coralStore;
@@ -21,11 +31,17 @@ export function getStore() {
return next(action);
};
+ const customListener = () => (next) => (action) => {
+ listeners.forEach((cb) => {cb(action);});
+ return next(action);
+ };
+
const middlewares = [
applyMiddleware(
getClient().middleware(),
thunk,
apolloErrorReporter,
+ customListener,
),
];
diff --git a/client/coral-ui/components/Tab.css b/client/coral-ui/components/Tab.css
index b3c026b3d..fc38d192d 100644
--- a/client/coral-ui/components/Tab.css
+++ b/client/coral-ui/components/Tab.css
@@ -68,3 +68,13 @@
pointer-events: none;
}
+
+@custom-media --small-viewport (max-width: 320px);
+
+@media (--small-viewport) {
+ .buttonSub, .buttonSubActive, .buttonSubActive:hover, .buttonSubActive:focus {
+ font-size: 0.96em;
+ padding-right: 5px;
+ padding-right: 5px;
+ }
+}
diff --git a/package.json b/package.json
index f3ba19d1f..65c802ba8 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"dotenv": "^4.0.0",
"ejs": "^2.5.6",
"env-rewrite": "^1.0.2",
+ "eventemitter2": "^4.1.2",
"express": "^4.15.2",
"express-session": "^1.15.1",
"file-loader": "^0.11.2",
diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js
index f7be6e701..40ee6e14c 100644
--- a/plugin-api/beta/client/hocs/withReaction.js
+++ b/plugin-api/beta/client/hocs/withReaction.js
@@ -54,6 +54,13 @@ export default (reaction) => (WrappedComponent) => {
id: fragmentId
});
+ if (!data) {
+ if (self) {
+ throw new Error(`Comment ${action.item_id} was not found`);
+ }
+ return;
+ }
+
// Add our comment from the mutation to the end.
let idx = data.action_summaries.findIndex(isReaction);
@@ -96,6 +103,13 @@ export default (reaction) => (WrappedComponent) => {
id: fragmentId
});
+ if (!data) {
+ if (self) {
+ throw new Error(`Comment ${action.item_id} was not found`);
+ }
+ return;
+ }
+
// Check whether we liked this comment.
const idx = data.action_summaries.findIndex(isReaction);
diff --git a/plugins/coral-plugin-viewing-options/client/translations.yml b/plugins/coral-plugin-viewing-options/client/translations.yml
new file mode 100644
index 000000000..6ab31fa1b
--- /dev/null
+++ b/plugins/coral-plugin-viewing-options/client/translations.yml
@@ -0,0 +1,4 @@
+en:
+ viewing_options: "Viewing Options"
+es:
+ viewing_options: "Opciones de visualización"
\ No newline at end of file
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tag.css b/plugins/talk-plugin-featured-comments/client/components/Tag.css
index 88a019b5d..7ade3d968 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tag.css
+++ b/plugins/talk-plugin-featured-comments/client/components/Tag.css
@@ -24,6 +24,15 @@
cursor: pointer;
}
+.noSelect {
+ -ms-user-select:none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -webkit-touch-callout:none;
+ user-select: none;
+ -webkit-tap-highlight-color:rgba(0,0,0,0);
+}
+
.tooltip {
top: 36px;
left: auto;
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tag.js b/plugins/talk-plugin-featured-comments/client/components/Tag.js
index b7ee8cd6b..ee2d13b19 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tag.js
+++ b/plugins/talk-plugin-featured-comments/client/components/Tag.js
@@ -15,13 +15,15 @@ export default class Tag extends React.Component {
}
- showTooltip = () => {
+ showTooltip = e => {
+ e.preventDefault();
this.setState({
tooltip: true
});
}
- hideTooltip = () => {
+ hideTooltip = (e) => {
+ e.preventDefault();
this.setState({
tooltip: false
});
@@ -30,10 +32,13 @@ export default class Tag extends React.Component {
render() {
const {tooltip} = this.state;
return(
-
+
{
isTagged(this.props.comment.tags, 'FEATURED') ? (
-
+
{t('talk-plugin-featured-comments.featured')}
) : null
diff --git a/plugins/talk-plugin-featured-comments/client/constants.js b/plugins/talk-plugin-featured-comments/client/constants.js
index 7aad99014..c03bf4839 100644
--- a/plugins/talk-plugin-featured-comments/client/constants.js
+++ b/plugins/talk-plugin-featured-comments/client/constants.js
@@ -1,2 +1,4 @@
-export const SHOW_TOOLTIP = 'SHOW_TOOLTIP';
-export const HIDE_TOOLTIP = 'HIDE_TOOLTIP';
+const prefix = 'TALK_FEATURED_COMMENTS';
+
+export const SHOW_TOOLTIP = `${prefix}_SHOW_TOOLTIP`;
+export const HIDE_TOOLTIP = `${prefix}_HIDE_TOOLTIP`;
diff --git a/plugins/talk-plugin-viewing-options/client/actions.js b/plugins/talk-plugin-viewing-options/client/actions.js
index 8a6473e3c..fbac224bf 100644
--- a/plugins/talk-plugin-viewing-options/client/actions.js
+++ b/plugins/talk-plugin-viewing-options/client/actions.js
@@ -1,9 +1,9 @@
-import {VIEWING_OPTIONS_OPEN, VIEWING_OPTIONS_CLOSE} from './constants';
+import {OPEN_MENU, CLOSE_MENU} from './constants';
-export const openViewingOptions = () => ({
- type: VIEWING_OPTIONS_OPEN
+export const openMenu = () => ({
+ type: OPEN_MENU,
});
-export const closeViewingOptions = () => ({
- type: VIEWING_OPTIONS_CLOSE
+export const closeMenu = () => ({
+ type: CLOSE_MENU,
});
diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css
index 81d373b95..c51d87f11 100644
--- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css
+++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css
@@ -5,6 +5,10 @@
composes: buttonReset from "coral-framework/styles/reset.css";
}
+.button:hover {
+ cursor: pointer;
+}
+
.list {
background: white;
position: absolute;
@@ -29,3 +33,27 @@
font-size: 14px;
vertical-align: middle;
}
+
+
+@custom-media --small-viewport (max-width: 425px);
+
+.filterText {
+ display: inline-block;
+}
+
+.filterIcon {
+ vertical-align: middle;
+ display: none;
+}
+
+@media (--small-viewport) {
+ .filterText {
+ display: none;
+ }
+ .filterIcon {
+ display: inline-block;
+ }
+}
+
+
+
diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js
index a235b1863..f05cc2eea 100644
--- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js
+++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js
@@ -1,21 +1,22 @@
import React from 'react';
import cn from 'classnames';
import styles from './ViewingOptions.css';
-import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
+import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
+import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
const ViewingOptions = (props) => {
const toggleOpen = () => {
if (!props.open) {
- props.openViewingOptions();
+ props.openMenu();
} else {
- props.closeViewingOptions();
+ props.closeMenu();
}
};
const handleClickOutside = () => {
if (props.open) {
- props.closeViewingOptions();
+ props.closeMenu();
}
};
@@ -23,7 +24,9 @@ const ViewingOptions = (props) => {
-
diff --git a/plugins/talk-plugin-viewing-options/client/constants.js b/plugins/talk-plugin-viewing-options/client/constants.js
index 586fe7a28..857a76e1b 100644
--- a/plugins/talk-plugin-viewing-options/client/constants.js
+++ b/plugins/talk-plugin-viewing-options/client/constants.js
@@ -1,2 +1,5 @@
-export const VIEWING_OPTIONS_OPEN = 'VIEWING_OPTIONS_OPEN';
-export const VIEWING_OPTIONS_CLOSE = 'VIEWING_OPTIONS_CLOSE';
+const prefix = 'TALK_VIEWING_OPTIONS';
+
+export const OPEN_MENU = `${prefix}_OPEN_MENU`;
+export const CLOSE_MENU = `${prefix}_CLOSE_MENU`;
+
diff --git a/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js
index a34ad91f6..22608828e 100644
--- a/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js
+++ b/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js
@@ -1,13 +1,13 @@
import {connect} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import ViewingOptions from '../components/ViewingOptions';
-import {openViewingOptions, closeViewingOptions} from '../actions';
+import {openMenu, closeMenu} from '../actions';
const mapStateToProps = ({talkPluginViewingOptions: state}) => ({
open: state.open
});
const mapDispatchToProps = (dispatch) =>
- bindActionCreators({openViewingOptions, closeViewingOptions}, dispatch);
+ bindActionCreators({openMenu, closeMenu}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(ViewingOptions);
diff --git a/plugins/talk-plugin-viewing-options/client/index.js b/plugins/talk-plugin-viewing-options/client/index.js
index 4e9001c1b..464a38d7d 100644
--- a/plugins/talk-plugin-viewing-options/client/index.js
+++ b/plugins/talk-plugin-viewing-options/client/index.js
@@ -1,9 +1,11 @@
import ViewingOptions from './containers/ViewingOptions';
import reducer from './reducer';
+import translations from './translations.yml';
export default {
reducer,
slots: {
streamFilter: [ViewingOptions]
- }
+ },
+ translations
};
diff --git a/plugins/talk-plugin-viewing-options/client/reducer.js b/plugins/talk-plugin-viewing-options/client/reducer.js
index ab9f077df..687de803a 100644
--- a/plugins/talk-plugin-viewing-options/client/reducer.js
+++ b/plugins/talk-plugin-viewing-options/client/reducer.js
@@ -1,17 +1,17 @@
-import {VIEWING_OPTIONS_OPEN, VIEWING_OPTIONS_CLOSE} from './constants';
+import {OPEN_MENU, CLOSE_MENU} from './constants';
const initialState = {
open: false
};
-export default function offTopic (state = initialState, action) {
+export default function reducer(state = initialState, action) {
switch (action.type) {
- case VIEWING_OPTIONS_OPEN:
+ case OPEN_MENU:
return {
...state,
open: true
};
- case VIEWING_OPTIONS_CLOSE:
+ case CLOSE_MENU:
return {
...state,
open: false
diff --git a/views/article.ejs b/views/article.ejs
index 73e4fc86b..d31653baa 100644
--- a/views/article.ejs
+++ b/views/article.ejs
@@ -30,6 +30,17 @@
asset_url: '<%= asset_url ? asset_url : '' %>',
asset_id: '<%= asset_id ? asset_id : '' %>',
auth_token: '',
+ /**
+ * You can listen to events using the example below.
+ * The argument passed is the event emitter from
+ * https://github.com/asyncly/EventEmitter2
+ *
+ * events: function(events) {
+ * events.onAny(function(eventName, data) {
+ * console.log(eventName, data);
+ * });
+ * },
+ */
plugin_config: {
/**
* You can disable rendering slot components of a plugin by doing:
diff --git a/yarn.lock b/yarn.lock
index d2177f384..255dc7415 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2957,6 +2957,10 @@ event-stream@~3.3.0:
stream-combiner "~0.0.4"
through "~2.3.1"
+eventemitter2@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15"
+
eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"