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/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css index 81d373b95..c51d87f11 100644 --- a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css +++ b/plugins/coral-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/coral-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js index a7b4be755..1d4708250 100644 --- a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js @@ -1,8 +1,9 @@ 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 = () => { @@ -23,7 +24,9 @@ const ViewingOptions = (props) => {
-
diff --git a/plugins/coral-plugin-viewing-options/client/index.js b/plugins/coral-plugin-viewing-options/client/index.js index 4e9001c1b..464a38d7d 100644 --- a/plugins/coral-plugin-viewing-options/client/index.js +++ b/plugins/coral-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/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/views/article.ejs b/views/article.ejs index d803b6295..f1a7ba5d9 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"