mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 16:10:30 +08:00
Merge branch 'master' into prefix-redux-actions-plugins
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
@@ -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(
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<AppRouter />
|
||||
</ApolloProvider>
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<AppRouter />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
, document.querySelector('#talk-embed-stream-container')
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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 <WrappedComponent
|
||||
{...this.props}
|
||||
emit={this.emit}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithEmit;
|
||||
};
|
||||
@@ -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 <Wrapped {...this.props} />;
|
||||
}
|
||||
return memoized;
|
||||
};
|
||||
|
||||
return (props) => {
|
||||
const Wrapped = getWrapped();
|
||||
return <Wrapped {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 <Wrapped {...props} />;
|
||||
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 <Wrapped {...this.props} />;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
<ClickOutside onClickOutside={handleClickOutside}>
|
||||
<div className={cn([styles.root, 'coral-plugin-viewing-options'])}>
|
||||
<div>
|
||||
<button className={styles.button} onClick={toggleOpen}>Viewing Options
|
||||
<button className={styles.button} onClick={toggleOpen}>
|
||||
<Icon className={styles.filterIcon} name="filter_list" />
|
||||
<span className={styles.filterText}>{t('viewing_options')}</span>
|
||||
{props.open ? <Icon name="arrow_drop_up" className={styles.icon}/> : <Icon name="arrow_drop_down" className={styles.icon}/>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
en:
|
||||
viewing_options: "Viewing Options"
|
||||
es:
|
||||
viewing_options: "Opciones de visualización"
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
<div onMouseEnter={this.showTooltip} onMouseLeave={this.hideTooltip} >
|
||||
<div className={styles.noSelect} onMouseEnter={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
|
||||
onTouchEnd={this.hideTooltip}>
|
||||
{
|
||||
isTagged(this.props.comment.tags, 'FEATURED') ? (
|
||||
<span className={cn(styles.tag, {[styles.on]: tooltip})}>
|
||||
<span
|
||||
className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
|
||||
{t('talk-plugin-featured-comments.featured')}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user