mirror of
https://github.com/wassname/talk.git
synced 2026-08-12 12:30:39 +08:00
Implement support for events
This commit is contained in:
@@ -5,15 +5,18 @@ 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 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();
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
@@ -42,11 +45,29 @@ if (!window.opener) {
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Pass any events through our parent.
|
||||
eventEmitter.onAny((eventName, value) => {
|
||||
pym.sendMessage('eventEmitter', JSON.stringify({eventName, value}));
|
||||
});
|
||||
|
||||
// Add a redux listener to pass through all actions to our event emitter.
|
||||
addListener((action) => {
|
||||
|
||||
// Ignore apollo actions.
|
||||
if (action.type.startsWith('APOLLO_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventEmitter.emit(`redux.action.${action.type}`, {action});
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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,11 @@ function configurePymParent(pymParent, opts) {
|
||||
window.open(url, '_blank').focus();
|
||||
});
|
||||
|
||||
pymParent.onMessage('eventEmitter', (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,23 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
export default (WrappedComponent) => {
|
||||
class WithCopyToClipboard extends React.Component {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
};
|
||||
|
||||
emit = (eventName, value, context) => {
|
||||
this.context.eventEmitter.emit(eventName, value, context);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <WrappedComponent
|
||||
{...this.props}
|
||||
emit={this.emit}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithCopyToClipboard;
|
||||
};
|
||||
@@ -6,8 +6,8 @@ import flatten from 'lodash/flatten';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
|
||||
import {getDefinitionName, getResponseErrors} from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
class ResponseErrors extends Error {
|
||||
constructor(errors) {
|
||||
@@ -43,10 +43,6 @@ export default (document, config = {}) => (WrappedComponent) => {
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
emit = (eventName, value, context) => {
|
||||
this.context.eventEmitter.emit(eventName, value, context);
|
||||
};
|
||||
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
memoized = null;
|
||||
|
||||
@@ -119,13 +115,21 @@ export default (document, config = {}) => (WrappedComponent) => {
|
||||
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});
|
||||
return Promise.resolve(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.context.eventEmitter.emit(`mutation.${name}.error`, {variables, error});
|
||||
throw new error;
|
||||
});
|
||||
};
|
||||
return config.props({...data, mutate});
|
||||
|
||||
@@ -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,141 @@ 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);
|
||||
|
||||
this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, networkStatus});
|
||||
}
|
||||
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) => {
|
||||
const subscrName = getDefinitionName(stmArgs.document);
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.subscribeToMore.${subscrName}`,
|
||||
{variables: stmArgs.variables});
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return args.data.subscribeToMore({
|
||||
...stmArgs,
|
||||
document: resolveFragments(stmArgs.document),
|
||||
onError: (err) => {
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.subscribeToMore.${subscrName}.error`,
|
||||
{variables: stmArgs.variables});
|
||||
|
||||
if (stmArgs.onErr) {
|
||||
return stmArgs.onErr(err);
|
||||
}
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
},
|
||||
fetchMore: (lmArgs) => {
|
||||
const fetchName = getDefinitionName(lmArgs.query);
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.fetchMore.${fetchName}`,
|
||||
{variables: lmArgs.variables});
|
||||
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
|
||||
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} />;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,12 +3,18 @@ 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));
|
||||
}
|
||||
|
||||
export function addListener(cb) {
|
||||
listeners.push(cb);
|
||||
}
|
||||
|
||||
export function getStore() {
|
||||
if (window.coralStore) {
|
||||
return window.coralStore;
|
||||
@@ -21,11 +27,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,
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -75,6 +75,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",
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
asset_url: '<%= asset_url ? asset_url : '' %>',
|
||||
asset_id: '<%= asset_id ? asset_id : '' %>',
|
||||
auth_token: '',
|
||||
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:
|
||||
|
||||
@@ -2961,6 +2961,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