Merge branch 'master' into stable-keys

This commit is contained in:
Kiwi
2017-09-11 18:51:18 +02:00
committed by GitHub
22 changed files with 518 additions and 210 deletions
+14 -14
View File
@@ -12,23 +12,23 @@ import {toast} from 'react-toastify';
import {createNotificationService} from './services/notification';
import {hideShortcutsNote} from './actions/moderation';
function hidrateStore({store, storage}) {
smoothscroll.polyfill();
function init({store, storage}) {
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
store.dispatch(hideShortcutsNote());
}
}
smoothscroll.polyfill();
async function main() {
const notification = createNotificationService(toast);
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
render(
<TalkProvider {...context}>
<App />
</TalkProvider>
, document.querySelector('#root')
);
}
const notification = createNotificationService(toast);
const context = createContext({reducers, graphqlExtension, pluginsConfig, notification});
// hidrate Store with external data.
hidrateStore(context);
render(
<TalkProvider {...context}>
<App />
</TalkProvider>
, document.querySelector('#root')
);
main();
@@ -92,12 +92,6 @@ const singleCommentFragment = gql`
const withCommentFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Comment_root on RootQuery {
me {
ignoredUsers {
id
}
}
__typename
me {
ignoredUsers {
id
@@ -215,6 +215,6 @@ const mapDispatchToProps = (dispatch) =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch((props) => !props.auth.checkedInitialLogin && props.config, renderComponent(Spinner)),
branch((props) => !props.auth.checkedInitialLogin, renderComponent(Spinner)),
withEmbedQuery,
)(EmbedContainer);
+35 -32
View File
@@ -10,11 +10,6 @@ import reducers from './reducers';
import TalkProvider from 'coral-framework/components/TalkProvider';
import pluginsConfig from 'pluginsConfig';
const context = createContext({reducers, graphqlExtension, pluginsConfig});
// TODO: move init code into `bootstrap` service after auth has been refactored.
const {store, pym} = context;
function inIframe() {
try {
return window.self !== window.top;
@@ -23,37 +18,45 @@ function inIframe() {
}
}
function init(config = {}) {
store.dispatch(addExternalConfig(config));
store.dispatch(checkLogin());
}
// TODO: move init code into `bootstrap` service after auth has been refactored.
function preInit({store, pym}) {
// Don't run this in the popup.
if (!window.opener) {
if (inIframe()) {
// TODO: This is popup specific code and needs to be refactored.
if (!inIframe()) {
store.dispatch(addExternalConfig({}));
store.dispatch(checkLogin());
return;
}
pym.onMessage('login', (token) => {
if (token) {
store.dispatch(handleAuthToken(token));
}
store.dispatch(checkLogin());
});
pym.onMessage('logout', () => {
store.dispatch(logout());
});
return new Promise((resolve) => {
pym.sendMessage('getConfig');
pym.onMessage('config', (config) => {
init(JSON.parse(config));
});
pym.onMessage('login', (token) => {
if (token) {
store.dispatch(handleAuthToken(token));
}
store.dispatch(addExternalConfig(config));
store.dispatch(checkLogin());
resolve();
});
pym.onMessage('logout', () => {
store.dispatch(logout());
});
} else {
init();
}
});
}
render(
<TalkProvider {...context}>
<AppRouter />
</TalkProvider>
, document.querySelector('#talk-embed-stream-container')
);
async function main() {
const context = await createContext({reducers, graphqlExtension, pluginsConfig, preInit});
render(
<TalkProvider {...context}>
<AppRouter />
</TalkProvider>
, document.querySelector('#talk-embed-stream-container')
);
}
main();
+4
View File
@@ -3,6 +3,7 @@ import pym from 'pym.js';
import EventEmitter from 'eventemitter2';
import {buildUrl} from 'coral-framework/utils/url';
import Snackbar from './Snackbar';
import {createStorage, connectStorageToPym} from 'coral-framework/services/storage';
const NOTIFICATION_OFFSET = 200;
@@ -134,6 +135,9 @@ export default class Stream {
// If the user clicks outside the embed, then tell the embed.
document.addEventListener('click', this.handleClick.bind(this), true);
// Listens to storage requests on pym and relay it to local storage.
connectStorageToPym(createStorage(), this.pym);
}
login(token) {
+1 -1
View File
@@ -83,7 +83,7 @@ Talk.render = (el, opts) => {
console.warn(
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
);
if (!window.location.origin) {
window.location.origin = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
}
+34 -12
View File
@@ -12,8 +12,10 @@ import {createPluginsService} from './plugins';
import {createNotificationService} from './notification';
import {createGraphQLRegistry} from './graphqlRegistry';
import globalFragments from 'coral-framework/graphql/fragments';
import {createStorage} from 'coral-framework/services/storage';
import {createStorage, createPymStorage} from 'coral-framework/services/storage';
import {createHistory} from 'coral-framework/services/history';
import {createIntrospection} from 'coral-framework/services/introspection';
import introspectionData from 'coral-framework/graphql/introspection.json';
/**
* getStaticConfiguration will return a singleton of the static configuration
@@ -60,17 +62,28 @@ const getAuthToken = (store, storage) => {
/**
* createContext setups and returns Talk dependencies that should be
* passed to `TalkProvider`.
* @param {Object} [config] configuration
* @param {Object} [config.reducers] extra reducers to add to redux
* @param {Array} [config.pluginsConfig] plugin configuration as returned by importing pluginsConfig
* @param {Object} [config.graphqlExtensions] additional extension to the graphql framework
* @param {Object} [config.notification] replace default notification service
* @return {Object} context
* @param {Object} [config] configuration
* @param {Object} [config.reducers] extra reducers to add to redux
* @param {Array} [config.pluginsConfig] plugin configuration as returned by importing pluginsConfig
* @param {Object} [config.graphqlExtensions] additional extension to the graphql framework
* @param {Object} [config.notification] replace default notification service
* @param {Function} [config.init] run initialization e.g. to hydrate redux store
* @param {Function} [config.preInit] same as init but run and resolve before init and plugins init
* @return {Object} context
*/
export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) {
export async function createContext({
reducers = {},
pluginsConfig = [],
graphqlExtension = {},
notification,
preInit,
init,
} = {}) {
const eventEmitter = new EventEmitter({wildcard: true});
const storage = createStorage();
const pymStorage = createPymStorage(pym);
const history = createHistory(BASE_PATH);
const introspection = createIntrospection(introspectionData);
let store = null;
const token = () => {
@@ -104,6 +117,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi
uri: `${BASE_PATH}api/v1/graph/ql`,
liveUri,
token,
introspectionData,
});
const plugins = createPluginsService(pluginsConfig);
const graphqlRegistry = createGraphQLRegistry(plugins.getSlotFragments.bind(plugins));
@@ -123,6 +137,8 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi
notification,
storage,
history,
introspection,
pymStorage,
};
// Load framework fragments.
@@ -155,8 +171,14 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi
createReduxEmitter(eventEmitter),
]);
return {
...context,
store,
};
context.store = store;
// Run pre initialization.
if (preInit) {
await preInit(context);
}
// Run initialization.
await Promise.all([init, plugins.executeInit(context)]);
return context;
}
+3 -3
View File
@@ -1,7 +1,6 @@
import ApolloClient, {addTypename, IntrospectionFragmentMatcher, createNetworkInterface} from 'apollo-client';
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
import MessageTypes from 'subscriptions-transport-ws/dist/message-types';
import introspectionQueryResultData from '../graphql/introspection.json';
// Redux middleware to report any errors to the console.
export const apolloErrorReporter = () => (next) => (action) => {
@@ -21,10 +20,11 @@ function resolveToken(token) {
* @param {string|function} [options.token] auth token
* @param {string} [options.uri] uri of the graphql server
* @param {string} [options.liveUri] uri of the graphql subscription server
* @param {Object} [options.introspectionData] introspection query result data
* @return {Object} apollo client
*/
export function createClient(options = {}) {
const {token, uri, liveUri} = options;
const {token, uri, liveUri, introspectionData} = options;
const wsClient = new SubscriptionClient(liveUri, {
reconnect: true,
lazy: true,
@@ -63,7 +63,7 @@ export function createClient(options = {}) {
const client = new ApolloClient({
connectToDevTools: true,
addTypename: true,
fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}),
fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData: introspectionData}),
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
@@ -0,0 +1,31 @@
class Introspection {
_enums=null;
constructor(data) {
this._enums = data.__schema.types
.filter((type) => type.kind === 'ENUM')
.reduce((obj, enumType) => {
obj[enumType.name] = enumType.enumValues.map((value) => value.name);
return obj;
}, {});
}
/**
* isValidEnumValue returns true when given enum and value exists.
* @param {string} name
* @param {string} value
* @return {boolean}
*/
isValidEnumValue(name, value) {
return this._enums[name] && this._enums[name].indexOf(value) >= 0;
}
}
/**
* createIntrospection returns a introspection service
* @param {Object} data introspection query data
* @return {Object} introspection service
*/
export function createIntrospection(data) {
return new Introspection(data);
}
@@ -182,6 +182,14 @@ class PluginsService {
.map((o) => ({[camelize(o.name)] : o.module.reducer}))
);
}
async executeInit(context) {
const results = this.plugins
.map((o) => o.module.init)
.filter((fn) => fn)
.map((fn) => fn(context));
await Promise.all(results);
}
}
/**
@@ -1,3 +1,4 @@
import uuid from 'uuid/v4';
function getStorage(type) {
let storage = window[type], x = '__storage_test__';
@@ -41,3 +42,91 @@ function getStorage(type) {
export function createStorage() {
return getStorage('localStorage');
}
/**
* Creates a storage that relay requests over to pym.
* This is the counterpart of `connectStorageToPym`.
* @param {string} pym pym
* @return {Object} storage
*/
export function createPymStorage(pym) {
// A Map of requestID => {resolve, reject}
const requests = {};
// Requests method with parameters over pym.
const call = (method, parameters) => {
const id = uuid();
return new Promise((resolve, reject) => {
requests[id] = {resolve, reject};
pym.sendMessage('pymStorage.request', JSON.stringify({id, method, parameters}));
});
};
// Receive successful responses.
pym.onMessage('pymStorage.response', (msg) => {
const {id, result} = JSON.parse(msg);
requests[id].resolve(result);
delete requests[id];
});
// Receive error responses.
pym.onMessage('pymStorage.error', (msg) => {
const {id, error} = JSON.parse(msg);
requests[id].reject(error);
delete requests[id];
});
return {
setItem: (key, value) => call('setItem', {key, value}),
getItem: (key, value) => call('getItem', {key, value}),
removeItem: (key) => call('removeItem', {key}),
};
}
/**
* Listens to `pym` and relay storage requests to `storage`.
* This is the counterpart of `createPymStorage`.
* @param {Object} storage storage to perform requests on
* @param {Object} pym pym to listen to storage requests
* @param {string} prefix namespace requests by prepending a prefix to the keys
*/
export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
pym.onMessage('pymStorage.request', (msg) => {
const {id, method, parameters} = JSON.parse(msg);
const {key, value} = parameters;
const prefixedKey = `${prefix}${key}`;
// Variable for the method return value.
let result;
const sendError = (error) => {
console.error(error);
pym.sendMessage('pymStorage.error', JSON.stringify({id, error}));
};
try {
switch(method) {
case 'setItem':
result = storage.setItem(prefixedKey, value);
break;
case 'getItem':
result = storage.getItem(prefixedKey);
break;
case 'removeItem':
result = storage.removeItem(prefixedKey);
break;
default:
sendError(`Unknown method ${method}`);
return;
}
}
catch(err) {
sendError(err.toString());
return;
}
pym.sendMessage('pymStorage.response', JSON.stringify({id, result}));
});
}