From 54ea0b1a3fbf9be4ca695cf3eb0d4dcc602713d3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 22 Aug 2017 21:59:41 +0700 Subject: [PATCH] Remove global dependency on store, rest, pym and apollo client --- client/coral-admin/src/actions/assets.js | 9 +- client/coral-admin/src/actions/auth.js | 24 +++-- client/coral-admin/src/actions/community.js | 13 ++- client/coral-admin/src/actions/install.js | 9 +- client/coral-admin/src/actions/settings.js | 9 +- client/coral-admin/src/actions/users.js | 13 ++- client/coral-admin/src/index.js | 21 +--- .../coral-embed-stream/src/actions/asset.js | 9 +- client/coral-embed-stream/src/actions/auth.js | 56 +++++------ .../coral-embed-stream/src/actions/stream.js | 9 +- client/coral-embed-stream/src/index.js | 30 +----- .../components/TalkProvider.js | 4 +- client/coral-framework/helpers/plugins.js | 13 +-- client/coral-framework/helpers/request.js | 84 ---------------- client/coral-framework/services/bootstrap.js | 92 +++++++++++++++++ client/coral-framework/services/client.js | 99 ++++++++++--------- client/coral-framework/services/events.js | 6 +- client/coral-framework/services/rest.js | 62 ++++++++++++ client/coral-framework/services/store.js | 61 ++---------- client/coral-framework/services/transport.js | 37 ------- 20 files changed, 304 insertions(+), 356 deletions(-) delete mode 100644 client/coral-framework/helpers/request.js create mode 100644 client/coral-framework/services/bootstrap.js create mode 100644 client/coral-framework/services/rest.js delete mode 100644 client/coral-framework/services/transport.js diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index b9ebe0ada..1ce6ad6ac 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -8,7 +8,6 @@ import { UPDATE_ASSETS } from '../constants/assets'; -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; /** @@ -17,9 +16,9 @@ import t from 'coral-framework/services/i18n'; // Fetch a page of assets // Get comments to fill each of the three lists on the mod queue -export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => { +export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => { dispatch({type: FETCH_ASSETS_REQUEST}); - return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) + return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) .then(({result, count}) => dispatch({type: FETCH_ASSETS_SUCCESS, assets: result, @@ -34,9 +33,9 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte // Update an asset state // Get comments to fill each of the three lists on the mod queue -export const updateAssetState = (id, closedAt) => (dispatch) => { +export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => { dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt}); - return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) .then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) .catch((error) => { console.error(error); diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 8c84a95ec..3433fc95a 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,8 +1,6 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; -import coralApi from 'coral-framework/helpers/request'; import * as Storage from 'coral-framework/helpers/storage'; -import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; import jwtDecode from 'jwt-decode'; @@ -10,7 +8,7 @@ import jwtDecode from 'jwt-decode'; // SIGN IN //============================================================================== -export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => { +export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client}) => { dispatch({type: actions.LOGIN_REQUEST}); const params = { @@ -27,7 +25,7 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => }; } - return coralApi('/auth/local', params) + return rest('/auth/local', params) .then(({user, token}) => { if (!user) { @@ -38,7 +36,7 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => } dispatch(handleAuthToken(token)); - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(user)); }) .catch((error) => { @@ -84,11 +82,11 @@ const forgotPasswordFailure = (error) => ({ error, }); -export const requestPasswordReset = (email) => (dispatch) => { +export const requestPasswordReset = (email) => (dispatch, _, {rest}) => { dispatch(forgotPasswordRequest(email)); const redirectUri = location.href; - return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) + return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) .then(() => dispatch(forgotPasswordSuccess())) .catch((error) => { console.error(error); @@ -116,9 +114,9 @@ const checkLoginFailure = (error) => ({ error }); -export const checkLogin = () => (dispatch) => { +export const checkLogin = () => (dispatch, _, {rest, client}) => { dispatch(checkLoginRequest()); - return coralApi('/auth') + return rest('/auth') .then(({user}) => { if (!user) { if (!bowser.safari && !bowser.ios) { @@ -127,7 +125,7 @@ export const checkLogin = () => (dispatch) => { return dispatch(checkLoginFailure('not logged in')); } - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(user)); }) .catch((error) => { @@ -141,12 +139,12 @@ export const checkLogin = () => (dispatch) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch) => { - return coralApi('/auth', {method: 'DELETE'}).then(() => { +export const logout = () => (dispatch, _, {rest, client}) => { + return rest('/auth', {method: 'DELETE'}).then(() => { Storage.removeItem('token'); // Reset the websocket. - resetWebsocket(); + client.resetWebsocket(); dispatch({type: actions.LOGOUT}); }); diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 70c9b7592..402a32e5d 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -14,13 +14,12 @@ import { HIDE_REJECT_USERNAME_DIALOG } from '../constants/community'; -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; -export const fetchAccounts = (query = {}) => (dispatch) => { +export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { dispatch(requestFetchAccounts()); - coralApi(`/users?${qs.stringify(query)}`) + rest(`/users?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) =>{ dispatch({ type: FETCH_COMMENTERS_SUCCESS, @@ -51,15 +50,15 @@ export const newPage = () => ({ type: COMMENTERS_NEW_PAGE }); -export const setRole = (id, role) => (dispatch) => { - return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}}) +export const setRole = (id, role) => (dispatch, _, {rest}) => { + return rest(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); }; -export const setCommenterStatus = (id, status) => (dispatch) => { - return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}}) +export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => { + return rest(`/users/${id}/status`, {method: 'POST', body: {status}}) .then(() => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index 6393c58dd..d7210782a 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -1,4 +1,3 @@ -import coralApi from 'coral-framework/helpers/request'; import * as actions from '../constants/install'; import validate from 'coral-framework/helpers/validate'; import errorMsj from 'coral-framework/helpers/error'; @@ -106,10 +105,10 @@ export const submitUser = () => (dispatch, getState) => { }); }; -export const finishInstall = () => (dispatch, getState) => { +export const finishInstall = () => (dispatch, getState, {rest}) => { const data = getState().install.data; dispatch(installRequest()); - return coralApi('/setup', {method: 'POST', body: data}) + return rest('/setup', {method: 'POST', body: data}) .then(() => { dispatch(installSuccess()); dispatch(nextStep()); @@ -129,9 +128,9 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST}); const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed}); const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error}); -export const checkInstall = (next) => (dispatch) => { +export const checkInstall = (next) => (dispatch, _, {rest}) => { dispatch(checkInstallRequest()); - coralApi('/setup') + rest('/setup') .then(({installed}) => { dispatch(checkInstallSuccess(installed)); if (installed) { diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index ca9062b9e..35f2013c9 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -1,4 +1,3 @@ -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; export const SETTINGS_LOADING = 'SETTINGS_LOADING'; @@ -14,9 +13,9 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; export const WORDLIST_UPDATED = 'WORDLIST_UPDATED'; export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED'; -export const fetchSettings = () => (dispatch) => { +export const fetchSettings = () => (dispatch, _, {rest}) => { dispatch({type: SETTINGS_LOADING}); - coralApi('/settings') + rest('/settings') .then((settings) => { dispatch({type: SETTINGS_RECEIVED, settings}); }) @@ -41,13 +40,13 @@ export const updateDomainlist = (listName, list) => { return {type: DOMAINLIST_UPDATED, listName, list}; }; -export const saveSettingsToServer = () => (dispatch, getState) => { +export const saveSettingsToServer = () => (dispatch, getState, {rest}) => { let settings = getState().settings; if (settings.charCount) { settings.charCount = parseInt(settings.charCount); } dispatch({type: SAVE_SETTINGS_LOADING}); - coralApi('/settings', {method: 'PUT', body: settings}) + rest('/settings', {method: 'PUT', body: settings}) .then(() => { dispatch({type: SAVE_SETTINGS_SUCCESS, settings}); }) diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index 7ba051a90..994a79a79 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,4 +1,3 @@ -import coralApi from '../../../coral-framework/helpers/request'; import * as userTypes from '../constants/users'; import t from 'coral-framework/services/i18n'; @@ -7,9 +6,9 @@ import t from 'coral-framework/services/i18n'; */ // change status of a user export const userStatusUpdate = (status, userId, commentId) => { - return (dispatch) => { + return (dispatch, _, {rest}) => { dispatch({type: userTypes.UPDATE_STATUS_REQUEST}); - return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) + return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) .then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res})) .catch((error) => { console.error(error); @@ -21,8 +20,8 @@ export const userStatusUpdate = (status, userId, commentId) => { // change status of a user export const sendNotificationEmail = (userId, subject, body) => { - return (dispatch) => { - return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}}) + return (dispatch, _, {rest}) => { + return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}}) .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); @@ -33,8 +32,8 @@ export const sendNotificationEmail = (userId, subject, body) => { // let a user edit their username export const enableUsernameEdit = (userId) => { - return (dispatch) => { - return coralApi(`/users/${userId}/username-enable`, {method: 'POST'}) + return (dispatch, _, {rest}) => { + return rest(`/users/${userId}/username-enable`, {method: 'POST'}) .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index ed9d537c0..e85cbed8c 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,34 +1,21 @@ import React from 'react'; import {render} from 'react-dom'; import smoothscroll from 'smoothscroll-polyfill'; -import EventEmitter from 'eventemitter2'; import TalkProvider from 'coral-framework/components/TalkProvider'; - -import {getClient} from 'coral-framework/services/client'; -import store from './services/store'; - +import {createContext} from 'coral-framework/services/bootstrap'; +import reducers from './reducers'; import App from './components/App'; - import 'react-mdl/extra/material.js'; import './graphql'; -import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import plugins from 'pluginsConfig'; -const eventEmitter = new EventEmitter(); -const client = getClient(); +const context = createContext(reducers, plugins); -// TODO: pass redux actions through the emitter. - -loadPluginsTranslations(); -injectPluginsReducers(); smoothscroll.polyfill(); render( diff --git a/client/coral-embed-stream/src/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js index 87b6fdd98..6acebeafb 100644 --- a/client/coral-embed-stream/src/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,5 +1,4 @@ import * as actions from '../constants/asset'; -import coralApi from 'coral-framework/helpers/request'; import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -12,10 +11,10 @@ const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_R const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings}); const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error}); -export const updateConfiguration = (newConfig) => (dispatch, getState) => { +export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) => { const assetId = getState().asset.id; dispatch(updateAssetSettingsRequest()); - coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) + rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) .then(() => { dispatch(addNotification('success', t('framework.success_update_settings'))); dispatch(updateAssetSettingsSuccess(newConfig)); @@ -26,10 +25,10 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => { }); }; -export const updateOpenStream = (closedBody) => (dispatch, getState) => { +export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => { const assetId = getState().asset.id; dispatch(fetchAssetRequest()); - coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) + rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) .then(() => { dispatch(addNotification('success', t('framework.success_update_settings'))); dispatch(fetchAssetSuccess(closedBody)); diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index 851889a9b..55f7eb546 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -2,11 +2,8 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; import * as Storage from 'coral-framework/helpers/storage'; -import coralApi, {base} from 'coral-framework/helpers/request'; -import pym from 'coral-framework/services/pym'; import {addNotification} from 'coral-framework/actions/notification'; -import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; export const showSignInDialog = () => ({ @@ -59,9 +56,9 @@ export const updateUsername = ({username}) => ({ username }); -export const createUsername = (userId, formData) => (dispatch) => { +export const createUsername = (userId, formData) => (dispatch, _, {rest}) => { dispatch(createUsernameRequest()); - coralApi('/account/username', {method: 'PUT', body: formData}) + rest('/account/username', {method: 'PUT', body: formData}) .then(() => { dispatch(createUsernameSuccess()); dispatch(hideCreateUsernameDialog()); @@ -123,10 +120,10 @@ export const handleAuthToken = (token) => (dispatch) => { //============================================================================== export const fetchSignIn = (formData) => { - return (dispatch) => { + return (dispatch, _, {rest}) => { dispatch(signInRequest()); - return coralApi('/auth/local', {method: 'POST', body: formData}) + return rest('/auth/local', {method: 'POST', body: formData}) .then(({token}) => { if (!bowser.safari && !bowser.ios) { dispatch(handleAuthToken(token)); @@ -171,10 +168,10 @@ const signInFacebookFailure = (error) => ({ error }); -export const fetchSignInFacebook = () => (dispatch) => { +export const fetchSignInFacebook = () => (dispatch, _, {rest}) => { dispatch(signInFacebookRequest()); window.open( - `${base}/auth/facebook`, + `${rest.uri}/auth/facebook`, 'Continue with Facebook', 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' ); @@ -188,10 +185,10 @@ const signUpFacebookRequest = () => ({ type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST }); -export const fetchSignUpFacebook = () => (dispatch) => { +export const fetchSignUpFacebook = () => (dispatch, _, {rest}) => { dispatch(signUpFacebookRequest()); window.open( - `${base}/auth/facebook`, + `${rest.uri}/auth/facebook`, 'Continue with Facebook', 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' ); @@ -220,11 +217,11 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = (formData) => (dispatch, getState) => { +export const fetchSignUp = (formData) => (dispatch, getState, {rest}) => { const redirectUri = getState().auth.redirectUri; dispatch(signUpRequest()); - coralApi('/users', { + rest('/users', { method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri} @@ -256,10 +253,10 @@ const forgotPasswordFailure = (error) => ({ error, }); -export const fetchForgotPassword = (email) => (dispatch, getState) => { +export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => { dispatch(forgotPasswordRequest(email)); const redirectUri = getState().auth.redirectUri; - coralApi('/account/password/reset', { + rest('/account/password/reset', { method: 'POST', body: {email, loc: redirectUri} }) @@ -275,16 +272,15 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch) => { - return coralApi('/auth', {method: 'DELETE'}).then(() => { - Storage.removeItem('token'); +export const logout = () => async (dispatch, _, {rest, client, pym}) => { + await rest('/auth', {method: 'DELETE'}); + Storage.removeItem('token'); - // Reset the websocket. - resetWebsocket(); + // Reset the websocket. + client.resetWebsocket(); - dispatch({type: actions.LOGOUT}); - pym.sendMessage('coral-auth-changed'); - }); + dispatch({type: actions.LOGOUT}); + pym.sendMessage('coral-auth-changed'); }; //============================================================================== @@ -300,9 +296,9 @@ const checkLoginSuccess = (user, isAdmin) => ({ isAdmin }); -export const checkLogin = () => (dispatch) => { +export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { dispatch(checkLoginRequest()); - coralApi('/auth') + rest('/auth') .then((result) => { if (!result.user) { Storage.removeItem('token'); @@ -310,7 +306,7 @@ export const checkLogin = () => (dispatch) => { } // Reset the websocket. - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(result.user)); pym.sendMessage('coral-auth-changed', JSON.stringify(result.user)); @@ -351,10 +347,10 @@ const verifyEmailFailure = () => ({ type: actions.VERIFY_EMAIL_FAILURE }); -export const requestConfirmEmail = (email) => (dispatch, getState) => { +export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => { const redirectUri = getState().auth.redirectUri; dispatch(verifyEmailRequest()); - return coralApi('/users/resend-verify', { + return rest('/users/resend-verify', { method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri} @@ -387,8 +383,8 @@ export const setRedirectUri = (uri) => ({ const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error}); const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS}); -export const editName = (username) => (dispatch) => { - return coralApi('/account/username', {method: 'PUT', body: {username}}) +export const editName = (username) => (dispatch, _, {rest}) => { + return rest('/account/username', {method: 'PUT', body: {username}}) .then(() => { dispatch(editUsernameSuccess()); dispatch(addNotification('success', t('framework.success_name_update'))); diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index c20760259..75f353b19 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -1,11 +1,10 @@ -import pym from 'coral-framework/services/pym'; import * as actions from '../constants/stream'; import {buildUrl} from 'coral-framework/utils/url'; import queryString from 'query-string'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); -export const viewAllComments = () => { +export const viewAllComments = () => (dispatch, _, {pym}) => { const search = queryString.stringify({ ...queryString.parse(location.search), @@ -24,10 +23,10 @@ export const viewAllComments = () => { pym.sendMessage('coral-view-all-comments'); } catch (e) { /* not sure if we're worried about old browsers */ } - return {type: actions.VIEW_ALL_COMMENTS}; + dispatch({type: actions.VIEW_ALL_COMMENTS}); }; -export const viewComment = (id) => { +export const viewComment = (id) => (dispatch, _, {pym}) => { const search = queryString.stringify({ ...queryString.parse(location.search), @@ -46,7 +45,7 @@ export const viewComment = (id) => { pym.sendMessage('coral-view-comment', id); } catch (e) { /* not sure if we're worried about old browsers */ } - return {type: actions.VIEW_COMMENT, id}; + dispatch({type: actions.VIEW_COMMENT, id}); }; export const addCommentClassName = (className) => ({ diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index a14ce8df8..ac38d2eab 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -4,24 +4,16 @@ import {render} from 'react-dom'; import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; import './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; -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 {createContext} from 'coral-framework/services/bootstrap'; import AppRouter from './AppRouter'; -import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import reducers from './reducers'; -import EventEmitter from 'eventemitter2'; import TalkProvider from 'coral-framework/components/TalkProvider'; import plugins from 'pluginsConfig'; -const store = getStore(); -const client = getClient(); -const eventEmitter = new EventEmitter({wildcard: true}); +const context = createContext(reducers, plugins); -loadPluginsTranslations(); -injectPluginsReducers(); -injectReducers(reducers); +// TODO: move init code into `bootstrap` service after auth has been refactored. +const {store, pym} = context; function inIframe() { try { @@ -57,23 +49,11 @@ 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( diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 275373c26..0f44af43f 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -12,9 +12,9 @@ class TalkProvider extends React.Component { } render() { - const {children, client, store, plugins} = this.props; + const {children, client, store} = this.props; return ( - + {children} ); diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 3173115c2..b73c89c64 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -6,8 +6,6 @@ import flattenDeep from 'lodash/flattenDeep'; import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import mapValues from 'lodash/mapValues'; -import {loadTranslations} from 'coral-framework/services/i18n'; -import {injectReducers} from 'coral-framework/services/store'; import {getDisplayName} from 'coral-framework/helpers/hoc'; import camelize from './camelize'; import plugins from 'pluginsConfig'; @@ -133,23 +131,18 @@ export function getModQueueConfigs() { .filter((o) => o)); } -function getTranslations() { +export function getTranslations(plugins) { return plugins .map((o) => o.module.translations) .filter((o) => o); } -export function loadPluginsTranslations() { - getTranslations().forEach((t) => loadTranslations(t)); -} - -export function injectPluginsReducers() { - const reducers = merge( +export function getReducers(plugins) { + return merge( ...plugins .filter((o) => o.module.reducer) .map((o) => ({[camelize(o.name)] : o.module.reducer})) ); - injectReducers(reducers); } function addMetaDataToSlotComponents() { diff --git a/client/coral-framework/helpers/request.js b/client/coral-framework/helpers/request.js deleted file mode 100644 index ffddc9c5e..000000000 --- a/client/coral-framework/helpers/request.js +++ /dev/null @@ -1,84 +0,0 @@ -import bowser from 'bowser'; -import * as Storage from './storage'; -import merge from 'lodash/merge'; -import {getStore} from 'coral-framework/services/store'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -/** - * getAuthToken returns the active auth token or null - * Note: this method does not have access to the cookie based token used by - * browsers that don't allow us to use cross domain iframe local storage. - * @return {string|null} - */ -export const getAuthToken = () => { - let state = getStore().getState(); - - if (state.config.auth_token) { - - // if an auth_token exists in config, use it. - return state.config.auth_token; - - } else if (!bowser.safari && !bowser.ios) { - - // Use local storage auth tokens where there's a stable api. - return Storage.getItem('token'); - } - - return null; -}; - -const buildOptions = (inputOptions = {}) => { - const defaultOptions = { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - credentials: 'same-origin' - }; - - let options = merge({}, defaultOptions, inputOptions); - - // Apply authToken header - let authToken = getAuthToken(); - if (authToken !== null) { - options.headers.Authorization = `Bearer ${authToken}`; - } - - if (options.method.toLowerCase() !== 'get') { - options.body = JSON.stringify(options.body); - } - - return options; -}; - -const handleResp = (res) => { - if (res.status > 399) { - return res.json().then((err) => { - let message = err.message || res.status; - const error = new Error(); - - if (err.error && err.error.metadata && err.error.metadata.message) { - error.metadata = err.error.metadata.message; - } - - if (err.error && err.error.translation_key) { - error.translation_key = err.error.translation_key; - } - - error.message = message; - error.status = res.status; - throw error; - }); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); - } -}; - -export const base = `${BASE_PATH}api/v1`; - -export default (url, options) => { - return fetch(`${base}${url}`, buildOptions(options)).then(handleResp); -}; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js new file mode 100644 index 000000000..6a18695f7 --- /dev/null +++ b/client/coral-framework/services/bootstrap.js @@ -0,0 +1,92 @@ +import {createStore} from './store'; +import {createClient, apolloErrorReporter} from './client'; +import pym from './pym'; +import EventEmitter from 'eventemitter2'; +import {createReduxEmitter} from './events'; +import {createRestClient} from './rest'; +import thunk from 'redux-thunk'; +import {loadTranslations} from './i18n'; +import {getTranslations, getReducers} from '../helpers/plugins'; +import bowser from 'bowser'; +import * as Storage from '../helpers/storage'; +import {BASE_PATH} from 'coral-framework/constants/url'; + +/** + * getAuthToken returns the active auth token or null + * Note: this method does not have access to the cookie based token used by + * browsers that don't allow us to use cross domain iframe local storage. + * @return {string|null} + */ +const getAuthToken = (store) => { + let state = store.getState(); + + if (state.config.auth_token) { + + // if an auth_token exists in config, use it. + return state.config.auth_token; + + } else if (!bowser.safari && !bowser.ios) { + + // Use local storage auth tokens where there's a stable api. + return Storage.getItem('token'); + } + + return null; +}; + +export function createContext(reducers, plugins) { + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + const eventEmitter = new EventEmitter({wildcard: true}); + let store = null; + const token = () => { + + // Try to get the token from localStorage. If it isn't here, it may + // be passed as a cookie. + + // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT + // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. + return getAuthToken(store); + }; + const rest = createRestClient({ + uri: `${BASE_PATH}api/v1`, + token, + }); + const client = createClient({ + uri: `${BASE_PATH}api/v1/graph/ql`, + liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, + token, + }); + const context = { + client, + pym, + plugins, + eventEmitter, + rest, + }; + + // Load plugin translations. + getTranslations(plugins).forEach((t) => loadTranslations(t)); + + // Pass any events through our parent. + eventEmitter.onAny((eventName, value) => { + pym.sendMessage('event', JSON.stringify({eventName, value})); + }); + + const finalReducers = { + ...reducers, + ...getReducers(plugins), + apollo: client.reducer(), + }; + + store = createStore(finalReducers, [ + client.middleware(), + thunk.withExtraArgument(context), + apolloErrorReporter, + createReduxEmitter(eventEmitter), + ]); + + return { + ...context, + store, + }; +} diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index 6f044f621..a354d68bc 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -1,64 +1,55 @@ -import ApolloClient, {addTypename, IntrospectionFragmentMatcher} from 'apollo-client'; -import {networkInterface} from './transport'; +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 {getAuthToken} from '../helpers/request'; import introspectionQueryResultData from '../graphql/introspection.json'; -import {BASE_PATH} from 'coral-framework/constants/url'; -let client, wsClient = null, wsClientToken = null; - -export function resetWebsocket() { - if (wsClient === null) { - - // Nothing to reset! - return; +// Redux middleware to report any errors to the console. +export const apolloErrorReporter = () => (next) => (action) => { + if (action.type === 'APOLLO_QUERY_ERROR') { + console.error(action.error); } + return next(action); +}; - // Close socket connection which will also unregister subscriptions on the server-side. - wsClient.close(); - - // Reconnect to the server. - wsClient.connect(); - - // Reregister all subscriptions (uses non public api). - // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171 - Object.keys(wsClient.operations).forEach((id) => { - wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options); - }); -} - -export function getClient(options = {}) { - if (client) { - return client; - } - - const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; - wsClient = new SubscriptionClient(`${protocol}://${location.host}${BASE_PATH}api/v1/live`, { +export function createClient(options = {}) { + const {token, uri, liveUri, ...apolloOptions} = options; + const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, connectionParams: { - get token() { - - wsClientToken = getAuthToken(); - - // Try to get the token from localStorage. If it isn't here, it may - // be passed as a cookie. - - // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT - // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. - return wsClientToken; - } + token, } }); + const networkInterface = createNetworkInterface({ + uri, + opts: { + credentials: 'same-origin' + } + }); + + networkInterface.use([{ + applyMiddleware(req, next) { + if (!req.options.headers) { + req.options.headers = {}; // Create the header object if needed. + } + + let authToken = typeof token === 'function' ? token() : token; + if (authToken) { + req.options.headers['authorization'] = `Bearer ${authToken}`; + } + + next(); + } + }]); + const networkInterfaceWithSubscriptions = addGraphQLSubscriptions( networkInterface, wsClient, ); - client = new ApolloClient({ - ...options, + const client = new ApolloClient({ + ...apolloOptions, connectToDevTools: true, addTypename: true, fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}), @@ -72,5 +63,25 @@ export function getClient(options = {}) { networkInterface: networkInterfaceWithSubscriptions, }); + client.resetWebsocket = () => { + if (wsClient === null) { + + // Nothing to reset! + return; + } + + // Close socket connection which will also unregister subscriptions on the server-side. + wsClient.close(); + + // Reconnect to the server. + wsClient.connect(); + + // Reregister all subscriptions (uses non public api). + // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171 + Object.keys(wsClient.operations).forEach((id) => { + wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options); + }); + }; + return client; } diff --git a/client/coral-framework/services/events.js b/client/coral-framework/services/events.js index a1a2f4448..f6a62763d 100644 --- a/client/coral-framework/services/events.js +++ b/client/coral-framework/services/events.js @@ -1,5 +1,5 @@ export function createReduxEmitter(eventEmitter) { - return (action) => { + return () => (next) => (action) => { // Handle apollo actions. if (action.type.startsWith('APOLLO_')) { @@ -7,8 +7,10 @@ export function createReduxEmitter(eventEmitter) { const {operationName, variables, result: {data}} = action; eventEmitter.emit(`subscription.${operationName}.data`, {variables, data}); } - return; + return next(action); } eventEmitter.emit(`action.${action.type}`, {action}); + + return next(action); }; } diff --git a/client/coral-framework/services/rest.js b/client/coral-framework/services/rest.js new file mode 100644 index 000000000..f879659ca --- /dev/null +++ b/client/coral-framework/services/rest.js @@ -0,0 +1,62 @@ +import merge from 'lodash/merge'; + +const buildOptions = (inputOptions = {}) => { + const defaultOptions = { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'same-origin' + }; + + const options = merge({}, defaultOptions, inputOptions); + if (options.method.toLowerCase() !== 'get') { + options.body = JSON.stringify(options.body); + } + + return options; +}; + +const handleResp = (res) => { + if (res.status > 399) { + return res.json().then((err) => { + let message = err.message || res.status; + const error = new Error(); + + if (err.error && err.error.metadata && err.error.metadata.message) { + error.metadata = err.error.metadata.message; + } + + if (err.error && err.error.translation_key) { + error.translation_key = err.error.translation_key; + } + + error.message = message; + error.status = res.status; + throw error; + }); + } else if (res.status === 204) { + return res.text(); + } else { + return res.json(); + } +}; + +export function createRestClient(options) { + const {token, uri} = options; + const client = (path, options) => { + const authToken = typeof token === 'function' ? token() : token; + let opts = options; + if (authToken) { + opts = merge({}, options, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + } + return fetch(`${uri}${path}`, buildOptions(opts)).then(handleResp); + }; + client.uri = uri; + return client; +} diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index ec2479d15..e5fc52503 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,66 +1,21 @@ -import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; -import thunk from 'redux-thunk'; -import {getClient} from './client'; +import {createStore as reduxCreateStore, combineReducers, applyMiddleware, compose} from 'redux'; -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; - } - - const apolloErrorReporter = () => (next) => (action) => { - if (action.type === 'APOLLO_QUERY_ERROR') { - console.error(action.error); - } - return next(action); - }; - - const customListener = () => (next) => (action) => { - listeners.forEach((cb) => {cb(action);}); - return next(action); - }; - - const middlewares = [ +export function createStore(reducers, middlewares = []) { + const enhancers = [ applyMiddleware( - getClient().middleware(), - thunk, - apolloErrorReporter, - customListener, + ...middlewares, ), ]; if (window.devToolsExtension) { // we can't have the last argument of compose() be undefined - middlewares.push(window.devToolsExtension()); + enhancers.push(window.devToolsExtension()); } - const coralReducers = { - apollo: getClient().reducer() - }; - - window.coralStore = createStore( - combineReducers(coralReducers), + return reduxCreateStore( + combineReducers(reducers), {}, - compose(...middlewares) + compose(...enhancers) ); - - window.coralStore.coralReducers = coralReducers; - - return window.coralStore; } diff --git a/client/coral-framework/services/transport.js b/client/coral-framework/services/transport.js deleted file mode 100644 index 060bfc34b..000000000 --- a/client/coral-framework/services/transport.js +++ /dev/null @@ -1,37 +0,0 @@ -import {createNetworkInterface} from 'apollo-client'; -import {getAuthToken} from '../helpers/request'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -//============================================================================== -// NETWORK INTERFACE -//============================================================================== - -const networkInterface = createNetworkInterface({ - uri: `${BASE_PATH}api/v1/graph/ql`, - opts: { - credentials: 'same-origin' - } -}); - -//============================================================================== -// MIDDLEWARES -//============================================================================== - -networkInterface.use([{ - applyMiddleware(req, next) { - if (!req.options.headers) { - req.options.headers = {}; // Create the header object if needed. - } - - let authToken = getAuthToken(); - if (authToken) { - req.options.headers['authorization'] = `Bearer ${authToken}`; - } - - next(); - } -}]); - -export { - networkInterface -};