From 0af5058b004303b0fd47adf31706940f7bd0820c Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 21 Aug 2017 09:22:09 +0200 Subject: [PATCH 01/31] Make client server-side rendering compatible To server-side render, we need to import our routes file in iris, i.e. in a Node context. The issue is that in a Node context window is undefined, navigator is undefined, etc. etc. I removed everything that requires window access from the import path when importing routes.js, while making sure the app still works as expected. This patch means we can `const App = require('../src/routes')` from Iris which sets us up to do SSR! --- src/actions/authentication.js | 19 +++++++++++-------- src/api/constants.js | 9 +++++++++ src/api/index.js | 10 ---------- src/components/listItems/index.js | 2 +- src/components/upsell/index.js | 2 +- src/index.js | 21 +++++++++++++-------- src/routes.js | 10 +++++----- src/views/homepage/index.js | 2 +- 8 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 src/api/constants.js diff --git a/src/actions/authentication.js b/src/actions/authentication.js index 5a4fba152c..1565b8bca7 100644 --- a/src/actions/authentication.js +++ b/src/actions/authentication.js @@ -1,6 +1,5 @@ // @flow import { track, set } from '../helpers/events'; -import { clearApolloStore } from '../api'; import { removeItemFromStorage, storeItem } from '../helpers/localStorage'; import Raven from 'raven-js'; @@ -8,13 +7,17 @@ export const logout = () => { track(`user`, `sign out`, null); // clear localStorage removeItemFromStorage('spectrum'); - // clear Apollo's query cache - clearApolloStore(); - // redirect to home page - window.location.href = - process.env.NODE_ENV === 'production' - ? '/auth/logout' - : 'http://localhost:3001/auth/logout'; + import('../api') + .then(module => module.clearApolloStore) + .then(clearApolloStore => { + // clear Apollo's query cache + clearApolloStore(); + // redirect to home page + window.location.href = + process.env.NODE_ENV === 'production' + ? '/auth/logout' + : 'http://localhost:3001/auth/logout'; + }); }; export const saveUserDataToLocalStorage = (user: Object) => dispatch => { diff --git a/src/api/constants.js b/src/api/constants.js new file mode 100644 index 0000000000..ab36529b17 --- /dev/null +++ b/src/api/constants.js @@ -0,0 +1,9 @@ +export const SERVER_URL = + process.env.NODE_ENV === 'production' + ? `${window.location.protocol}//${window.location.host}` + : 'http://localhost:3001'; + +export const PUBLIC_STRIPE_KEY = + process.env.NODE_ENV === 'production' + ? 'pk_live_viV7X5XXD1sw8aN2NgQjiff6' + : 'pk_test_8aqk2JeScufGk1zAMe5GxaRq'; diff --git a/src/api/index.js b/src/api/index.js index dd49dfbf25..6afa4eb018 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -95,13 +95,3 @@ export const clearApolloStore = () => { console.log('error clearing store'); } }; - -export const SERVER_URL = - process.env.NODE_ENV === 'production' - ? `${window.location.protocol}//${window.location.host}` - : 'http://localhost:3001'; - -export const PUBLIC_STRIPE_KEY = - process.env.NODE_ENV === 'production' - ? 'pk_live_viV7X5XXD1sw8aN2NgQjiff6' - : 'pk_test_8aqk2JeScufGk1zAMe5GxaRq'; diff --git a/src/components/listItems/index.js b/src/components/listItems/index.js index 77fd7af9a5..d8e3cc4287 100644 --- a/src/components/listItems/index.js +++ b/src/components/listItems/index.js @@ -15,7 +15,7 @@ import Badge from '../badges'; import { Avatar } from '../avatar'; import { Button } from '../buttons'; import { convertTimestampToDate } from '../../helpers/utils'; -import { PUBLIC_STRIPE_KEY } from '../../api'; +import { PUBLIC_STRIPE_KEY } from '../../api/constants'; import { payInvoiceMutation } from '../../api/invoice'; import { addToastWithTimeout } from '../../actions/toasts'; import { diff --git a/src/components/upsell/index.js b/src/components/upsell/index.js index e8a1ff84e2..c81b8f282e 100644 --- a/src/components/upsell/index.js +++ b/src/components/upsell/index.js @@ -9,7 +9,7 @@ import compose from 'recompose/compose'; import Icon from '../../components/icons'; import FullscreenView from '../../components/fullscreenView'; import { getItemFromStorage, storeItem } from '../../helpers/localStorage'; -import { SERVER_URL, PUBLIC_STRIPE_KEY } from '../../api'; +import { SERVER_URL, PUBLIC_STRIPE_KEY } from '../../api/constants'; import { addToastWithTimeout } from '../../actions/toasts'; import { openModal } from '../../actions/modals'; import { Avatar } from '../avatar'; diff --git a/src/index.js b/src/index.js index 064509c762..954c7a1201 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import { ThemeProvider } from 'styled-components'; //$FlowFixMe import { ApolloProvider } from 'react-apollo'; import queryString from 'query-string'; +import { Router } from 'react-router'; import { history } from './helpers/history'; import { client } from './api'; import { initStore } from './store'; @@ -50,18 +51,22 @@ function render() { window.location.pathname === '/notifications') ) { return ReactDOM.render( - - - , + + + + + , document.querySelector('#root') ); } else { return ReactDOM.render( - - - - - , + + + + + + + , document.querySelector('#root') ); } diff --git a/src/routes.js b/src/routes.js index 2d7d3dfd24..5a42a9e50e 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,12 +1,12 @@ // @flow import React, { Component } from 'react'; //$FlowFixMe -import { Router, Route, Switch, Redirect } from 'react-router'; +import { Route, Switch, Redirect } from 'react-router'; //$FlowFixMe -import styled from 'styled-components'; +import styled, { ThemeProvider } from 'styled-components'; import generateMetaInfo from 'shared/generate-meta-info'; +import { theme } from './components/theme'; import { FlexCol } from './components/globals'; -import { history } from './helpers/history'; import ScrollManager from './components/scrollManager'; import Head from './components/head'; import ModalRoot from './components/modals/modalRoot'; @@ -51,7 +51,7 @@ class Routes extends Component { const { title, description } = generateMetaInfo(); return ( - + {/* Default meta tags, get overriden by anything further down the tree */} @@ -119,7 +119,7 @@ class Routes extends Component { - + ); } } diff --git a/src/views/homepage/index.js b/src/views/homepage/index.js index 376d5d22c8..c9cd9cda07 100644 --- a/src/views/homepage/index.js +++ b/src/views/homepage/index.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { track } from '../../helpers/events'; import Icon from '../../components/icons'; import { FlexCol, FlexRow } from '../../components/globals'; -import { SERVER_URL } from '../../api'; +import { SERVER_URL } from '../../api/constants'; import { storeItem, getItemFromStorage } from '../../helpers/localStorage'; import { SectionOne, From f3d3ff7d069adf1b07efe7e6271770a1a84f440c Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 21 Aug 2017 11:40:39 +0200 Subject: [PATCH 02/31] Make frontend Redux setup server-side renderable These changes make it possible to create a Redux store on the server with the server-side Apollo client and render our App in Node. --- src/helpers/events.js | 30 ----------------------- src/helpers/sentry-redux-middleware.js | 33 ++++++++++++++++++++++++++ src/index.js | 26 ++++++++++++++++---- src/reducers/index.js | 26 ++++++++++---------- src/store/index.js | 16 ++++++------- 5 files changed, 76 insertions(+), 55 deletions(-) create mode 100644 src/helpers/sentry-redux-middleware.js diff --git a/src/helpers/events.js b/src/helpers/events.js index 2d2fb2c47c..ee5cefcc45 100644 --- a/src/helpers/events.js +++ b/src/helpers/events.js @@ -55,33 +55,3 @@ export const track = (category, action, label) => { } } }; - -export const crashReporter = store => next => action => { - // Handle THROW_ERROR actions - if (action.type === 'THROW_ERROR') { - console.error('Caught an exception!', action.err); - if (process.env.NODE_ENV !== 'development') { - Raven.captureException(action.err, { - extra: { - action, - state: store.getState(), - }, - }); - } - } - - try { - return next(action); - } catch (err) { - console.error('Caught an exception!', err); - if (process.env.NODE_ENV !== 'development') { - Raven.captureException(err, { - extra: { - action, - state: store.getState(), - }, - }); - } - throw err; - } -}; diff --git a/src/helpers/sentry-redux-middleware.js b/src/helpers/sentry-redux-middleware.js new file mode 100644 index 0000000000..01dad9f7dc --- /dev/null +++ b/src/helpers/sentry-redux-middleware.js @@ -0,0 +1,33 @@ +import Raven from 'raven-js'; + +const crashReporter = store => next => action => { + // Handle THROW_ERROR actions + if (action.type === 'THROW_ERROR') { + console.error('Caught an exception!', action.err); + if (process.env.NODE_ENV !== 'development') { + Raven.captureException(action.err, { + extra: { + action, + state: store.getState(), + }, + }); + } + } + + try { + return next(action); + } catch (err) { + console.error('Caught an exception!', err); + if (process.env.NODE_ENV !== 'development') { + Raven.captureException(err, { + extra: { + action, + state: store.getState(), + }, + }); + } + throw err; + } +}; + +export default crashReporter; diff --git a/src/index.js b/src/index.js index 954c7a1201..da8f285b9a 100644 --- a/src/index.js +++ b/src/index.js @@ -32,13 +32,29 @@ if (thread) { const existingUser = getItemFromStorage('spectrum'); let store; if (existingUser) { - store = initStore({ - users: { - currentUser: existingUser.currentUser, + store = initStore( + { + users: { + currentUser: existingUser.currentUser, + }, }, - }); + { + middleware: [client.middleware()], + reducers: { + apollo: client.reducer(), + }, + } + ); } else { - store = initStore({}); + store = initStore( + {}, + { + middleware: [client.middleware()], + reducers: { + apollo: client.reducer(), + }, + } + ); } function render() { diff --git a/src/reducers/index.js b/src/reducers/index.js index 90ef65392f..db12c5d507 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,5 +1,4 @@ import { combineReducers } from 'redux'; -import { client } from '../api'; import users from './users'; import composer from './composer'; import modals from './modals'; @@ -8,15 +7,18 @@ import directMessageThreads from './directMessageThreads'; import gallery from './gallery'; import newUserOnboarding from './newUserOnboarding'; -const apollo = client.reducer(); +// Allow dependency injection of extra reducers, we need this for SSR +const getReducers = extraReducers => { + return combineReducers({ + users, + modals, + toasts, + directMessageThreads, + gallery, + composer, + newUserOnboarding, + ...extraReducers, + }); +}; -export default combineReducers({ - users, - modals, - toasts, - directMessageThreads, - gallery, - apollo, - composer, - newUserOnboarding, -}); +export default getReducers; diff --git a/src/store/index.js b/src/store/index.js index 85fae801e7..2b65d3be75 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,9 +1,8 @@ /* eslint-disable */ import { createStore, compose, applyMiddleware } from 'redux'; import thunkMiddleware from 'redux-thunk'; -import { crashReporter } from '../helpers/events'; -import reducers from '../reducers'; -import { client } from '../api'; +import crashReporter from '../helpers/sentry-redux-middleware'; +import getReducers from '../reducers'; // this enables the chrome devtools for redux only in development const composeEnhancers = @@ -13,21 +12,22 @@ const composeEnhancers = compose; // init the store with the thunkMiddleware which allows us to make async actions play nicely with the store -export const initStore = initialState => { +// Allow dependency injection of extra reducers and middleware, we need this for SSR +export const initStore = (initialState, { middleware, reducers }) => { if (initialState) { return createStore( - reducers, + getReducers(reducers), initialState, composeEnhancers( - applyMiddleware(client.middleware(), thunkMiddleware, crashReporter) + applyMiddleware(...middleware, thunkMiddleware, crashReporter) ) ); } else { return createStore( - reducers, + getReducers(reducers), {}, composeEnhancers( - applyMiddleware(client.middleware(), thunkMiddleware, crashReporter) + applyMiddleware(...middleware, thunkMiddleware, crashReporter) ) ); } From 8d06e4aa3239d1ae0c91b67129550dec172ed35e Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 21 Aug 2017 14:06:11 +0200 Subject: [PATCH 03/31] First working version of serving HTML --- iris/index.js | 109 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/api/index.js | 1 + yarn.lock | 24 ++++++++++- 4 files changed, 135 insertions(+), 1 deletion(-) diff --git a/iris/index.js b/iris/index.js index aeb9924387..8544776616 100644 --- a/iris/index.js +++ b/iris/index.js @@ -10,6 +10,7 @@ import fs from 'fs'; import { createServer } from 'http'; //$FlowFixMe import express from 'express'; +import * as graphql from 'graphql'; import schema from './schema'; import { init as initPassport } from './authentication.js'; @@ -34,6 +35,114 @@ app.use('/auth', authRoutes); import apiRoutes from './routes/api'; app.use('/api', apiRoutes); +global.window = { + location: { + protocol: 'https:', + host: 'spectrum.chat', + hash: '', + }, +}; +var LocalStorage = require('node-localstorage').LocalStorage, + localStorage = new LocalStorage('./test'); +global.localStorage = localStorage; +global.navigator = { + userAgent: '', +}; +const Routes = require('../src/routes').default; +import React from 'react'; +import ReactDOM from 'react-dom/server'; +import { ServerStyleSheet } from 'styled-components'; +import { + ApolloClient, + createNetworkInterface, + ApolloProvider, + renderToStringWithData, +} from 'react-apollo'; +import { StaticRouter } from 'react-router'; +import { createStore } from 'redux'; +import { createLocalInterface } from 'apollo-local-query'; + +import { initStore } from '../src/store'; + +function Html({ content, state, styleElement }) { + return ( + + + + {styleElement} + + +
+
${content}
` + ) + .replace( + '', + `${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}` + ); res.status(200); - res.send(`\n${ReactDOM.renderToStaticMarkup(html)}`); + res.send(final); res.end(); }) .catch(err => { From 5f33a9b74809182e774bf0764ea0fcd327dec7ef Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 23 Aug 2017 17:48:17 +0200 Subject: [PATCH 05/31] Refactor --- iris/index.js | 128 ++------------------------------------ iris/renderer/get-html.js | 31 +++++++++ iris/renderer/index.js | 102 ++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 122 deletions(-) create mode 100644 iris/renderer/get-html.js create mode 100644 iris/renderer/index.js diff --git a/iris/index.js b/iris/index.js index 06d7eccccf..d8c4702ff3 100644 --- a/iris/index.js +++ b/iris/index.js @@ -35,133 +35,17 @@ app.use('/auth', authRoutes); import apiRoutes from './routes/api'; app.use('/api', apiRoutes); -global.window = { - location: { - protocol: 'https:', - host: 'spectrum.chat', - hash: '', - }, -}; -var LocalStorage = require('node-localstorage').LocalStorage, - localStorage = new LocalStorage('./test'); -global.localStorage = localStorage; -global.navigator = { - userAgent: '', -}; -const Routes = require('../src/routes').default; -import React from 'react'; -import ReactDOM from 'react-dom/server'; -import { ServerStyleSheet } from 'styled-components'; -import { - ApolloClient, - createNetworkInterface, - ApolloProvider, - renderToStringWithData, -} from 'react-apollo'; -import { StaticRouter } from 'react-router'; -import { createStore } from 'redux'; -import { createLocalInterface } from 'apollo-local-query'; -import Helmet from 'react-helmet'; - -import { initStore } from '../src/store'; -const html = fs - .readFileSync(path.resolve(__dirname, '..', 'build', 'index.html')) - .toString(); - -app.use( - express.static(path.resolve(__dirname, '..', 'build'), { index: false }) -); -app.get('*', function(req, res) { - const client = new ApolloClient({ - ssrMode: true, - networkInterface: createLocalInterface(graphql, schema, { - context: { - loaders: createLoaders(), - }, - }), - }); - const store = initStore( - { - // users: { - // currentUser: existingUser.currentUser, - // }, - }, - { - middleware: [client.middleware()], - reducers: { - apollo: client.reducer(), - }, - } - ); - const context = {}; - // The client-side app will instead use - const frontend = ( - - - - - - ); - const sheet = new ServerStyleSheet(); - renderToStringWithData(sheet.collectStyles(frontend)) - .then(content => { - // We are ready to render for real - const initialState = store.getState(); - const helmet = Helmet.renderStatic(); - const final = html - .replace( - '
', - `${sheet.getStyleTags()}
${content}
` - ) - .replace( - '', - `${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}` - ); - res.status(200); - res.send(final); - res.end(); - }) - .catch(err => { - res.status(500); - res.end(); - console.log(err); - throw err; - }); -}); - // In production use express to serve the React app // In development this is done by react-scripts, which starts its own server -if (IS_PROD) { - const { graphql } = require('graphql'); - // Load index.html into memory - var index = fs - .readFileSync(path.resolve(__dirname, '..', 'build', 'index.html')) - .toString(); +if (IS_PROD || process.env.DEV_SSR) { + console.log('Enabled server-side rendering'); + const renderer = require('./renderer').default; app.use( express.static(path.resolve(__dirname, '..', 'build'), { index: false }) ); - app.get('*', function(req, res) { - getMeta(req.url, (query: string): Promise => - graphql(schema, query, undefined, { - loaders: createLoaders(), - user: req.user, - }) - ).then(({ title, description, extra }) => { - // In production inject the meta title and description - res.send( - index - // Replace "Spectrum" with proper title, but make sure to not replace the twitter site:name - // (which is set to Spectrum.chat) - .replace(/Spectrum(?!\.chat)/g, title) - // Replace "Where communities live." with proper description for page - .replace(/Where communities live\./g, description) - // Add any extra meta tags at the end - .replace(//g, extra || '') - ); - }); - }); + app.get('*', renderer); +} else { + console.log('Server-side rendering disabled for development'); } import type { Loader } from './loaders/types'; diff --git a/iris/renderer/get-html.js b/iris/renderer/get-html.js new file mode 100644 index 0000000000..9875a2f7a9 --- /dev/null +++ b/iris/renderer/get-html.js @@ -0,0 +1,31 @@ +// @flow +import fs from 'fs'; +import path from 'path'; + +const html = fs + .readFileSync(path.resolve(__dirname, '..', '..', 'build', 'index.html')) + .toString(); + +type Arguments = { + styleTags: string, + metaTags: string, + state: Object, + content: string, +}; + +export const getHTML = ({ styleTags, metaTags, state, content }: Arguments) => { + // TODO: Proper sanitization + // NOTE(@mxstbr): There's some library by Yahoo (I think) + // specifically for this purpose + const sanitizedState = JSON.stringify(state).replace(/ + .replace( + '
', + `
${content}
` + ) + // Inject the meta and style tags at the end of the + .replace('', `${metaTags}${styleTags}`) + ); +}; diff --git a/iris/renderer/index.js b/iris/renderer/index.js new file mode 100644 index 0000000000..7620fa1d27 --- /dev/null +++ b/iris/renderer/index.js @@ -0,0 +1,102 @@ +// @flow +// Server-side renderer for our React code +import React from 'react'; +import ReactDOM from 'react-dom/server'; +import { ServerStyleSheet } from 'styled-components'; +import { + ApolloClient, + createNetworkInterface, + ApolloProvider, + renderToStringWithData, +} from 'react-apollo'; +import { StaticRouter } from 'react-router'; +import { createStore } from 'redux'; +import { createLocalInterface } from 'apollo-local-query'; +import Helmet from 'react-helmet'; + +import * as graphql from 'graphql'; +import schema from '../schema'; +import createLoaders from '../loaders'; +import { getHTML } from './get-html'; + +// Gotta shim all the browser stuff we use +global.window = { + location: { + protocol: 'https:', + host: 'spectrum.chat', + hash: '', + }, +}; +var LocalStorage = require('node-localstorage').LocalStorage, + localStorage = new LocalStorage('./test'); +global.localStorage = localStorage; +global.navigator = { + userAgent: '', +}; +const Routes = require('../../src/routes').default; +import { initStore } from '../../src/store'; + +const renderer = (req, res) => { + // Create an Apollo Client with a local network interface + const client = new ApolloClient({ + ssrMode: true, + networkInterface: createLocalInterface(graphql, schema, { + context: { + loaders: createLoaders(), + user: req.user, + }, + }), + }); + // Create the Redux store + const store = initStore( + { + users: { + currentUser: req.user, + }, + }, + // Inject the server-side client's middleware and reducer + { + middleware: [client.middleware()], + reducers: { + apollo: client.reducer(), + }, + } + ); + // TODO(@mxstbr): Fix context, whatever it's for + const context = {}; + // The client-side app will instead use + const frontend = ( + + + + + + ); + // Initialise the styled-components stylesheet and wrap the app with it + const sheet = new ServerStyleSheet(); + renderToStringWithData(sheet.collectStyles(frontend)) + .then(content => { + // Get the resulting data + const state = store.getState(); + const helmet = Helmet.renderStatic(); + res.status(200); + // Compile the HTML and send it down + res.send( + getHTML({ + content, + state, + styleTags: sheet.getStyleTags(), + metaTags: `${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}`, + }) + ); + res.end(); + }) + .catch(err => { + console.log(err); + res.status(500); + res.end(); + throw err; + }); +}; + +export default renderer; From 0645bf34a5db8a7015159626a77e17d659c1a574 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 23 Aug 2017 17:48:33 +0200 Subject: [PATCH 06/31] Fix src env variable --- src/api/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/index.js b/src/api/index.js index a799749a61..e2835c24d5 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -44,7 +44,7 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ export const client = new ApolloClient({ networkInterface: networkInterfaceWithSubscriptions, fragmentMatcher, - initialState: window.__APOLLO_STATE__, + initialState: window.__SERVER_STATE__.apollo, queryDeduplication: true, dataIdFromObject: result => { if (result.__typename) { From 3a5f56c18f526f84b45001e430e7f7673548c69f Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 23 Aug 2017 17:49:16 +0200 Subject: [PATCH 07/31] Fix comment --- iris/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iris/index.js b/iris/index.js index d8c4702ff3..d8236c4c05 100644 --- a/iris/index.js +++ b/iris/index.js @@ -35,8 +35,8 @@ app.use('/auth', authRoutes); import apiRoutes from './routes/api'; app.use('/api', apiRoutes); -// In production use express to serve the React app -// In development this is done by react-scripts, which starts its own server +// In production use express to server-side render the React app +// In development we don't server-side render to get live reloading etc. if (IS_PROD || process.env.DEV_SSR) { console.log('Enabled server-side rendering'); const renderer = require('./renderer').default; From 1c91323c509efd0a1d69e32025261fe65a939877 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 23 Aug 2017 18:30:36 +0200 Subject: [PATCH 08/31] Modularization --- iris/renderer/browser-shim.js | 16 ++++++++++++++++ iris/renderer/index.js | 25 ++++++++----------------- 2 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 iris/renderer/browser-shim.js diff --git a/iris/renderer/browser-shim.js b/iris/renderer/browser-shim.js new file mode 100644 index 0000000000..3e9d9c986a --- /dev/null +++ b/iris/renderer/browser-shim.js @@ -0,0 +1,16 @@ +// @flow +// Shim some browser stuff we use in the client for server-side rendering +// NOTE(@mxstbr): We should be getting rid of this over time +global.window = { + location: { + protocol: 'https:', + host: 'spectrum.chat', + hash: '', + }, +}; +var LocalStorage = require('node-localstorage').LocalStorage, + localStorage = new LocalStorage('./test'); +global.localStorage = localStorage; +global.navigator = { + userAgent: '', +}; diff --git a/iris/renderer/index.js b/iris/renderer/index.js index 7620fa1d27..0611363b31 100644 --- a/iris/renderer/index.js +++ b/iris/renderer/index.js @@ -13,26 +13,14 @@ import { StaticRouter } from 'react-router'; import { createStore } from 'redux'; import { createLocalInterface } from 'apollo-local-query'; import Helmet from 'react-helmet'; - import * as graphql from 'graphql'; + import schema from '../schema'; import createLoaders from '../loaders'; import { getHTML } from './get-html'; -// Gotta shim all the browser stuff we use -global.window = { - location: { - protocol: 'https:', - host: 'spectrum.chat', - hash: '', - }, -}; -var LocalStorage = require('node-localstorage').LocalStorage, - localStorage = new LocalStorage('./test'); -global.localStorage = localStorage; -global.navigator = { - userAgent: '', -}; +// Browser shim has to come before any client imports +import './browser-shim'; const Routes = require('../../src/routes').default; import { initStore } from '../../src/store'; @@ -79,14 +67,17 @@ const renderer = (req, res) => { // Get the resulting data const state = store.getState(); const helmet = Helmet.renderStatic(); - res.status(200); // Compile the HTML and send it down + res.status(200); res.send( getHTML({ content, state, styleTags: sheet.getStyleTags(), - metaTags: `${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}`, + metaTags: + helmet.title.toString() + + helmet.meta.toString() + + helmet.link.toString(), }) ); res.end(); From 036aff7330d97d32a7adcd9dccf798e15e14410d Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 23 Aug 2017 18:49:43 +0200 Subject: [PATCH 09/31] Maybe implement rehydration? --- iris/renderer/index.js | 25 ++++++++++++------------- src/api/index.js | 2 +- src/index.js | 35 +++++++++++++---------------------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/iris/renderer/index.js b/iris/renderer/index.js index 0611363b31..adf82fb396 100644 --- a/iris/renderer/index.js +++ b/iris/renderer/index.js @@ -35,21 +35,20 @@ const renderer = (req, res) => { }, }), }); - // Create the Redux store - const store = initStore( - { - users: { - currentUser: req.user, - }, + // Define the initial redux state + const initialReduxState = { + users: { + currentUser: req.user, }, + }; + // Create the Redux store + const store = initStore(initialReduxState, { // Inject the server-side client's middleware and reducer - { - middleware: [client.middleware()], - reducers: { - apollo: client.reducer(), - }, - } - ); + middleware: [client.middleware()], + reducers: { + apollo: client.reducer(), + }, + }); // TODO(@mxstbr): Fix context, whatever it's for const context = {}; // The client-side app will instead use diff --git a/src/api/index.js b/src/api/index.js index e2835c24d5..f9ce73f15e 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -44,7 +44,7 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ export const client = new ApolloClient({ networkInterface: networkInterfaceWithSubscriptions, fragmentMatcher, - initialState: window.__SERVER_STATE__.apollo, + initialState: window.__SERVER_STATE__ && window.__SERVER_STATE__.apollo, queryDeduplication: true, dataIdFromObject: result => { if (result.__typename) { diff --git a/src/index.js b/src/index.js index da8f285b9a..a5cb9a9db0 100644 --- a/src/index.js +++ b/src/index.js @@ -30,33 +30,24 @@ if (thread) { } const existingUser = getItemFromStorage('spectrum'); -let store; +let initialState; if (existingUser) { - store = initStore( - { - users: { - currentUser: existingUser.currentUser, - }, + initialState = { + users: { + currentUser: existingUser.currentUser, }, - { - middleware: [client.middleware()], - reducers: { - apollo: client.reducer(), - }, - } - ); + }; } else { - store = initStore( - {}, - { - middleware: [client.middleware()], - reducers: { - apollo: client.reducer(), - }, - } - ); + initialState = {}; } +const store = initStore(window.__SERVER_STATE__ || initialState, { + middleware: [client.middleware()], + reducers: { + apollo: client.reducer(), + }, +}); + function render() { // if user is not stored in localStorage and they visit a blacklist url if ( From 74e18521bd73b688d6a4b4f41c0c85c4ed7fa2e8 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 23 Aug 2017 19:02:58 +0200 Subject: [PATCH 10/31] Make it work after building --- iris/authentication.js | 4 +--- iris/models/slackImport.js | 4 +--- iris/mutations/invoice.js | 4 +--- iris/mutations/recurringPayment.js | 4 +--- iris/utils/s3.js | 4 +--- package.json | 2 +- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/iris/authentication.js b/iris/authentication.js index 187e46ccc7..cac5295734 100644 --- a/iris/authentication.js +++ b/iris/authentication.js @@ -3,9 +3,7 @@ const env = require('node-env-file'); const IS_PROD = process.env.NODE_ENV === 'production'; const path = require('path'); -if (!IS_PROD) { - env(path.resolve(__dirname, './.env'), { raise: false }); -} +env(path.resolve(__dirname, './.env'), { raise: false }); // $FlowFixMe const passport = require('passport'); // $FlowFixMe diff --git a/iris/models/slackImport.js b/iris/models/slackImport.js index 38b7d75a83..004dff8f68 100644 --- a/iris/models/slackImport.js +++ b/iris/models/slackImport.js @@ -9,9 +9,7 @@ const slackImportQueue = createQueue('slack import'); const env = require('node-env-file'); const IS_PROD = process.env.NODE_ENV === 'production'; const path = require('path'); -if (!IS_PROD) { - env(path.resolve(__dirname, '../.env'), { raise: false }); -} +env(path.resolve(__dirname, '../.env'), { raise: false }); let SLACK_SECRET = process.env.SLACK_SECRET; if (!IS_PROD) { diff --git a/iris/mutations/invoice.js b/iris/mutations/invoice.js index 0a661e7ee3..bd4bfea070 100644 --- a/iris/mutations/invoice.js +++ b/iris/mutations/invoice.js @@ -6,9 +6,7 @@ const env = require('node-env-file'); const IS_PROD = process.env.NODE_ENV === 'production'; // $FlowFixMe const path = require('path'); -if (!IS_PROD) { - env(path.resolve(__dirname, '../.env'), { raise: false }); -} +env(path.resolve(__dirname, '../.env'), { raise: false }); const STRIPE_TOKEN = process.env.STRIPE_TOKEN; const stripe = require('stripe')(STRIPE_TOKEN), currency = 'USD'; diff --git a/iris/mutations/recurringPayment.js b/iris/mutations/recurringPayment.js index a87d20afbf..444ef0a293 100644 --- a/iris/mutations/recurringPayment.js +++ b/iris/mutations/recurringPayment.js @@ -6,9 +6,7 @@ const env = require('node-env-file'); const IS_PROD = process.env.NODE_ENV === 'production'; // $FlowFixMe const path = require('path'); -if (!IS_PROD) { - env(path.resolve(__dirname, '../.env'), { raise: false }); -} +env(path.resolve(__dirname, '../.env'), { raise: false }); const STRIPE_TOKEN = process.env.STRIPE_TOKEN; diff --git a/iris/utils/s3.js b/iris/utils/s3.js index c153d2ab1c..606c3a8d83 100644 --- a/iris/utils/s3.js +++ b/iris/utils/s3.js @@ -5,9 +5,7 @@ const Uploader = require('s3-image-uploader'); const env = require('node-env-file'); const IS_PROD = process.env.NODE_ENV === 'production'; const path = require('path'); -if (!IS_PROD) { - env(path.resolve(__dirname, '../.env'), { raise: false }); -} +env(path.resolve(__dirname, '../.env'), { raise: false }); let S3_TOKEN = process.env.S3_TOKEN; let S3_SECRET = process.env.S3_SECRET; diff --git a/package.json b/package.json index f6215c8814..696e98f600 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "dev:iris": "cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=iris*,-iris:resolvers cross-env DIR=iris backpack", "dev:athena": "cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=athena* cross-env DIR=athena backpack", "dev:hermes": "cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=hermes* cross-env DIR=hermes backpack", - "build": "npm run build:iris && npm run build:client", + "build": "npm run build:client && npm run build:iris", "prebuild:iris": "rimraf build-iris", "build:iris": "cross-env NODE_PATH=./ cross-env DIR=iris backpack build", "prebuild:athena": "rimraf build-athena", From c31d96c4e8dd0e7bda1d67909d59b3fc91453469 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Aug 2017 11:39:32 -0600 Subject: [PATCH 11/31] Swithc to localstorage memory --- iris/renderer/browser-shim.js | 3 +-- package.json | 1 + yarn.lock | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/iris/renderer/browser-shim.js b/iris/renderer/browser-shim.js index 3e9d9c986a..949a12bb7b 100644 --- a/iris/renderer/browser-shim.js +++ b/iris/renderer/browser-shim.js @@ -8,8 +8,7 @@ global.window = { hash: '', }, }; -var LocalStorage = require('node-localstorage').LocalStorage, - localStorage = new LocalStorage('./test'); +var localStorage = require('localstorage-memory'); global.localStorage = localStorage; global.navigator = { userAgent: '', diff --git a/package.json b/package.json index 696e98f600..0b4e419d1b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "immutability-helper": "^2.2.0", "json-stringify-pretty-compact": "^1.0.4", "linkify-it": "^2.0.3", + "localstorage-memory": "^1.0.2", "lodash": "^4.17.4", "moment": "^2.18.1", "node-env-file": "^0.1.8", diff --git a/yarn.lock b/yarn.lock index 360804849c..c498274964 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5040,6 +5040,10 @@ loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +localstorage-memory@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/localstorage-memory/-/localstorage-memory-1.0.2.tgz#cd4a8f210e55dd519c929f4b4cc82829b58f9a51" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" From 73e6297f78c02db7090a0d31ae090b7c5c5cb0d1 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Aug 2017 11:46:33 -0600 Subject: [PATCH 12/31] localstorage noop --- iris/renderer/browser-shim.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iris/renderer/browser-shim.js b/iris/renderer/browser-shim.js index 949a12bb7b..1aae6f77da 100644 --- a/iris/renderer/browser-shim.js +++ b/iris/renderer/browser-shim.js @@ -8,8 +8,10 @@ global.window = { hash: '', }, }; -var localStorage = require('localstorage-memory'); -global.localStorage = localStorage; +global.localStorage = { + getItem: () => null, + setItem: () => {}, +}; global.navigator = { userAgent: '', }; From 84c2c2dbb9dde81b1cef85ac90d2c35cfe6dc3bd Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 31 Aug 2017 14:27:06 +0200 Subject: [PATCH 13/31] Ignore vim .swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d3ff99139b..7aadf6fa70 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ build-hermes package-lock.json .vscode dump.rdb +*.swp From 0830fc67c038d8a22351045bb99733fef8192ad9 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 31 Aug 2017 14:35:26 +0200 Subject: [PATCH 14/31] Fix s --- iris/renderer/index.js | 6 +++++- src/routes.js | 24 ++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/iris/renderer/index.js b/iris/renderer/index.js index adf82fb396..248b2315ca 100644 --- a/iris/renderer/index.js +++ b/iris/renderer/index.js @@ -49,7 +49,6 @@ const renderer = (req, res) => { apollo: client.reducer(), }, }); - // TODO(@mxstbr): Fix context, whatever it's for const context = {}; // The client-side app will instead use const frontend = ( @@ -63,6 +62,11 @@ const renderer = (req, res) => { const sheet = new ServerStyleSheet(); renderToStringWithData(sheet.collectStyles(frontend)) .then(content => { + if (context.url) { + // Somewhere a `` was rendered, so let's redirect server-side + res.redirect(301, context.url); + return; + } // Get the resulting data const state = store.getState(); const helmet = Helmet.renderStatic(); diff --git a/src/routes.js b/src/routes.js index bc0a4f66d7..85f87927ce 100644 --- a/src/routes.js +++ b/src/routes.js @@ -82,7 +82,9 @@ class Routes extends Component { ( + + ))} /> {/* Public Business Pages */} @@ -102,15 +104,21 @@ class Routes extends Component { ( + + ))} /> ( + + ))} /> ( + + ))} /> } /> @@ -118,11 +126,15 @@ class Routes extends Component { ( + + ))} /> ( + + ))} /> {/* From 208b98423c4db20aa008c381f0a7e9e6eb393c52 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 31 Aug 2017 14:39:24 +0200 Subject: [PATCH 15/31] Add rel="nofollow" --- src/views/navbar/components/profileDropdown.js | 2 +- src/views/navbar/index.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/views/navbar/components/profileDropdown.js b/src/views/navbar/components/profileDropdown.js index f38070cbc1..782e56d3e4 100644 --- a/src/views/navbar/components/profileDropdown.js +++ b/src/views/navbar/components/profileDropdown.js @@ -43,7 +43,7 @@ export const ProfileDropdown = props => { {props.user.username && ( - + My Settings diff --git a/src/views/navbar/index.js b/src/views/navbar/index.js index bf3a9973e9..f1100b0082 100644 --- a/src/views/navbar/index.js +++ b/src/views/navbar/index.js @@ -366,6 +366,7 @@ class Navbar extends Component { Date: Fri, 1 Sep 2017 11:14:58 +0200 Subject: [PATCH 16/31] Force fetch delay on client --- src/api/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/index.js b/src/api/index.js index f9ce73f15e..10845be091 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -45,6 +45,7 @@ export const client = new ApolloClient({ networkInterface: networkInterfaceWithSubscriptions, fragmentMatcher, initialState: window.__SERVER_STATE__ && window.__SERVER_STATE__.apollo, + ssrForceFetchDelay: 100, queryDeduplication: true, dataIdFromObject: result => { if (result.__typename) { From d8a3484b8a1fc389399013878b332042bbc209fc Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Fri, 1 Sep 2017 15:56:43 +0200 Subject: [PATCH 17/31] Fix some issues with SSR --- .babelrc | 2 +- config-overrides.js | 4 +++- package.json | 5 +++-- src/index.js | 14 +++++--------- yarn.lock | 19 +++++++++++++++---- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.babelrc b/.babelrc index 5a474c5322..b7b4f18e87 100644 --- a/.babelrc +++ b/.babelrc @@ -4,5 +4,5 @@ "node": "current" } }]], - "plugins": ["transform-flow-strip-types", "transform-object-rest-spread", "babel-plugin-transform-react-jsx", "syntax-dynamic-import"] + "plugins": [["styled-components", { ssr: true }], "transform-flow-strip-types", "transform-object-rest-spread", "babel-plugin-transform-react-jsx", "syntax-dynamic-import"] } diff --git a/config-overrides.js b/config-overrides.js index b8ecd97e62..8841eb1e33 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -8,6 +8,7 @@ const rewireStyledComponents = require('react-app-rewire-styled-components'); const swPrecachePlugin = require('sw-precache-webpack-plugin'); const fs = require('fs'); const match = require('micromatch'); +const WriteFilePlugin = require('write-file-webpack-plugin'); const isServiceWorkerPlugin = plugin => plugin instanceof swPrecachePlugin; const whitelist = path => new RegExp(`^(?!\/${path}).*`); @@ -29,5 +30,6 @@ const setCustomSwPrecacheOptions = config => { module.exports = function override(config, env) { setCustomSwPrecacheOptions(config); - return rewireStyledComponents(config, env); + config.plugins.push(WriteFilePlugin()); + return rewireStyledComponents(config, env, { ssr: true }); }; diff --git a/package.json b/package.json index 99c3467086..e7e7329b40 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "raw-loader": "^0.5.1", "react-scripts": "^1.0.0", "rimraf": "^2.6.1", - "uuid": "^3.0.1" + "uuid": "^3.0.1", + "write-file-webpack-plugin": "^4.1.0" }, "dependencies": { "apollo-local-query": "^0.3.0", @@ -95,7 +96,7 @@ "string-replace-to-array": "^1.0.3", "stripe": "^4.15.0", "striptags": "2.x", - "styled-components": "2.x", + "styled-components": "2.1.2", "subscriptions-transport-ws": "^0.7.0", "web-push": "^3.2.2" }, diff --git a/src/index.js b/src/index.js index 3a6d6eb578..88f44e874b 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; //$FlowFixMe -import { ThemeProvider } from 'styled-components'; -//$FlowFixMe import { ApolloProvider } from 'react-apollo'; //$FlowFixMe import { Router } from 'react-router'; @@ -51,13 +49,11 @@ const store = initStore(window.__SERVER_STATE__ || initialState, { function render() { return ReactDOM.render( - - - - - - - , + + + + + , document.querySelector('#root') ); } diff --git a/yarn.lock b/yarn.lock index fe0fdf3073..5e112a1a54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3280,7 +3280,7 @@ fileset@^2.0.2: glob "^7.0.3" minimatch "^3.0.3" -filesize@3.5.10: +filesize@3.5.10, filesize@^3.2.1: version "3.5.10" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f" @@ -5357,7 +5357,7 @@ lodash.values@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -5654,7 +5654,7 @@ moment-timezone@^0.5.0: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.15.2, moment@^2.17.1, moment@^2.18.1: +"moment@>= 2.9.0", moment@^2.11.2, moment@^2.15.2, moment@^2.17.1, moment@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -8142,7 +8142,7 @@ style-loader@0.18.2: loader-utils "^1.0.2" schema-utils "^0.3.0" -styled-components@2.x, styled-components@^2.0.0: +styled-components@2.1.2, styled-components@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.1.2.tgz#bb419978e1287c5d0d88fa9106b2dd75f66a324c" dependencies: @@ -8984,6 +8984,17 @@ write-file-atomic@^1.1.2, write-file-atomic@^1.1.4: imurmurhash "^0.1.4" slide "^1.1.5" +write-file-webpack-plugin@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/write-file-webpack-plugin/-/write-file-webpack-plugin-4.1.0.tgz#ed6ae9b54b68719c4ef4899fba70ce7cbdad0154" + dependencies: + chalk "^1.1.1" + debug "^2.6.8" + filesize "^3.2.1" + lodash "^4.5.1" + mkdirp "^0.5.1" + moment "^2.11.2" + write@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" From 2411a44afcd987d3c6b80b15d9a72fa2ef7245e1 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Fri, 1 Sep 2017 17:43:16 +0200 Subject: [PATCH 18/31] Fix window.innerHeight bug --- src/routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes.js b/src/routes.js index 85f87927ce..373bcdb30c 100644 --- a/src/routes.js +++ b/src/routes.js @@ -46,7 +46,7 @@ const Body = styled(FlexCol)` @media (max-width: 768px) { height: 100vh; - max-height: ${window.innerHeight}px; + max-height: 100vh; } `; From 63c53ad833fb9f1b17f8081a8a4f811b0abce8ba Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Fri, 1 Sep 2017 17:57:26 +0200 Subject: [PATCH 19/31] Work around react-router bug to make SSR work `react-router` has a bug where a `` with just a query parameter in the `to` prop is treated like an absolute link, but only on the server by the `StaticRouter`. This works around the issue by transforming `` to ``. Reference issue: https://github.com/ReactTraining/react-router/issues/5488 --- src/components/profile/thread.js | 2 +- src/components/threadFeedCard/index.js | 35 +++++++++-------- src/helpers/notifications.js | 12 ++---- .../components/newMessageNotification.js | 11 +++--- .../components/newReactionNotification.js | 24 ++++++++---- src/views/notifications/utils.js | 38 ++++++++----------- 6 files changed, 57 insertions(+), 65 deletions(-) diff --git a/src/components/profile/thread.js b/src/components/profile/thread.js index c52c574b20..b5d3526256 100644 --- a/src/components/profile/thread.js +++ b/src/components/profile/thread.js @@ -19,7 +19,7 @@ class ThreadWithData extends Component { } return ( - + => { return ( - + - - - {props.data.content.title} - - {props.isPinned && + + {props.data.content.title} + {props.isPinned && ( - } + + )} {attachmentsExist && attachments.map((attachment, i) => { @@ -68,17 +67,17 @@ const ThreadFeedCardPure = (props: Object): React$Element => { })} {participantsExist && } - {props.data.messageCount > 0 - ? - - - {props.data.messageCount} - - - : - - Fresh thread! - } + {props.data.messageCount > 0 ? ( + + + {props.data.messageCount} + + ) : ( + + + Fresh thread! + + )} diff --git a/src/helpers/notifications.js b/src/helpers/notifications.js index 7e2b03d1eb..f44afe8cb7 100644 --- a/src/helpers/notifications.js +++ b/src/helpers/notifications.js @@ -45,7 +45,7 @@ export const constructMessage = notification => { return ( {sender.name} replied to your{' '} - thread: + thread: ); default: @@ -80,11 +80,7 @@ export const constructContent = notification => { const { type, sender, content } = notification; switch (type) { case 'NEW_THREAD': - return ( -

- {content.excerpt} -

- ); + return

{content.excerpt}

; case 'NEW_MESSAGE': return (
@@ -93,9 +89,7 @@ export const constructContent = notification => {
- - {content.excerpt} - + {content.excerpt}
); default: diff --git a/src/views/notifications/components/newMessageNotification.js b/src/views/notifications/components/newMessageNotification.js index 8642bb380d..7786fe9106 100644 --- a/src/views/notifications/components/newMessageNotification.js +++ b/src/views/notifications/components/newMessageNotification.js @@ -47,9 +47,7 @@ const renderBubbleHeader = (sender: Object, me: boolean) => { return ( - - {me ? 'You' : sender.name} - + {me ? 'You' : sender.name} ); @@ -84,7 +82,7 @@ export const NewMessageNotification = ({ notification, currentUser }) => { return ( - + @@ -92,7 +90,8 @@ export const NewMessageNotification = ({ notification, currentUser }) => { - {' '}{actors.asString} {event} {context.asString} {date}{' '} + {' '} + {actors.asString} {event} {context.asString} {date}{' '} @@ -194,7 +193,7 @@ export const MiniNewMessageNotification = ({ return ( - + diff --git a/src/views/notifications/components/newReactionNotification.js b/src/views/notifications/components/newReactionNotification.js index c269d0c9e6..c9c385822d 100644 --- a/src/views/notifications/components/newReactionNotification.js +++ b/src/views/notifications/components/newReactionNotification.js @@ -40,7 +40,9 @@ export const NewReactionNotification = ({ notification, currentUser }) => { return ( - + @@ -48,7 +50,8 @@ export const NewReactionNotification = ({ notification, currentUser }) => { - {' '}{actors.asString} {event} {context.asString} {date}{' '} + {' '} + {actors.asString} {event} {context.asString} {date}{' '} @@ -59,7 +62,7 @@ export const NewReactionNotification = ({ notification, currentUser }) => { - {message.messageType === 'text' && + {message.messageType === 'text' && ( { color={'text.reverse'} /> - } - {message.messageType === 'media' && + + )} + {message.messageType === 'media' && ( { color={'text.reverse'} /> - } + + )} @@ -135,7 +140,9 @@ export const MiniNewReactionNotification = ({ return ( - + @@ -143,7 +150,8 @@ export const MiniNewReactionNotification = ({ - {' '}{actors.asString} {event} {context.asString}{' '} + {' '} + {actors.asString} {event} {context.asString}{' '} {messageStr && `"${messageStr}"`} {date}{' '} diff --git a/src/views/notifications/utils.js b/src/views/notifications/utils.js index ca856757d2..d013b0f1b3 100644 --- a/src/views/notifications/utils.js +++ b/src/views/notifications/utils.js @@ -48,23 +48,23 @@ export const parseNotification = notification => { }); }; -export const renderBubbleHeader = actor => +export const renderBubbleHeader = actor => ( - - {actor.name} - + {actor.name} {actor.isAdmin && } {actor.isPro && } - ; + +); -export const renderAvatar = actor => +export const renderAvatar = actor => ( - ; + +); const actorsToString = actors => { // reverse to show the most recent first @@ -78,9 +78,7 @@ const actorsToString = actors => { if (actors.length === 1) { return ( - - {`${names[0]}`} - + {`${names[0]}`} ); } else if (actors.length === 2) { @@ -165,11 +163,7 @@ export const parseEvent = event => { export const parseNotificationDate = date => { const now = new Date().getTime(); const timestamp = new Date(date).getTime(); - return ( - - · {timeDifferenceShort(now, timestamp)} - - ); + return · {timeDifferenceShort(now, timestamp)}; }; const threadToString = (context, currentUser) => { @@ -177,8 +171,9 @@ const threadToString = (context, currentUser) => { const str = isCreator ? 'in your thread' : 'in'; return ( - {' '}{str}{' '} - + {' '} + {str}{' '} + {context.payload.content.title} @@ -192,17 +187,14 @@ const messageToString = context => { const communityToString = context => { return ( - {' '}{context.payload.name} + {' '} + {context.payload.name} ); }; const channelToString = context => { - return ( - - {' '}{context.payload.name} - - ); + return {context.payload.name}; }; export const parseContext = (context, currentUser) => { From 45da4a48cfd04f40026da44b645fd3c38e11a371 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Sat, 2 Sep 2017 09:22:33 +0200 Subject: [PATCH 20/31] Share apollo client options --- iris/renderer/index.js | 2 + shared/graphql/apollo-client-options.js | 49 +++++++++++++++++++++++++ src/api/index.js | 48 +++--------------------- 3 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 shared/graphql/apollo-client-options.js diff --git a/iris/renderer/index.js b/iris/renderer/index.js index 248b2315ca..7c5bfcdfc4 100644 --- a/iris/renderer/index.js +++ b/iris/renderer/index.js @@ -15,6 +15,7 @@ import { createLocalInterface } from 'apollo-local-query'; import Helmet from 'react-helmet'; import * as graphql from 'graphql'; +import getSharedApolloClientOptions from 'shared/graphql/apollo-client-options'; import schema from '../schema'; import createLoaders from '../loaders'; import { getHTML } from './get-html'; @@ -34,6 +35,7 @@ const renderer = (req, res) => { user: req.user, }, }), + ...getSharedApolloClientOptions(), }); // Define the initial redux state const initialReduxState = { diff --git a/shared/graphql/apollo-client-options.js b/shared/graphql/apollo-client-options.js new file mode 100644 index 0000000000..b8a726f789 --- /dev/null +++ b/shared/graphql/apollo-client-options.js @@ -0,0 +1,49 @@ +// @flow +import { toIdValue } from 'react-apollo'; + +const getSharedApolloClientOptions = client => ({ + queryDeduplication: true, + dataIdFromObject: result => { + if (result.__typename) { + // Custom Community cache key based on slug + if (result.__typename === 'Community' && !!result.slug) { + return `${result.__typename}:${result.slug}`; + } + // Custom Channel cache key based on slug and community slug + if ( + result.__typename === 'Channel' && + !!result.slug && + !!result.community && + !!result.community.slug + ) { + return `${result.__typename}:${result.community.slug}:${result.slug}`; + } + // This was copied from the default dataIdFromObject + if (result.id !== undefined) { + return `${result.__typename}:${result.id}`; + } + if (result._id !== undefined) { + return `${result.__typename}:${result._id}`; + } + } + return null; + }, + customResolvers: { + Query: { + thread: (_, { id }) => + toIdValue(client.dataIdFromObject({ __typename: 'Thread', id })), + community: (_, { slug }) => + toIdValue(client.dataIdFromObject({ __typename: 'Community', slug })), + channel: (_, { channelSlug, communitySlug }) => + toIdValue( + client.dataIdFromObject({ + __typename: 'Channel', + slug: channelSlug, + community: { slug: communitySlug }, + }) + ), + }, + }, +}); + +export default getSharedApolloClientOptions; diff --git a/src/api/index.js b/src/api/index.js index 10845be091..936f3d72c9 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -13,6 +13,7 @@ import { addGraphQLSubscriptions, } from 'subscriptions-transport-ws'; import introspectionQueryResultData from './schema.json'; +import getSharedApolloClientOptions from 'shared/graphql/apollo-client-options'; const IS_PROD = process.env.NODE_ENV === 'production'; const wsClient = new SubscriptionClient( @@ -44,50 +45,11 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ export const client = new ApolloClient({ networkInterface: networkInterfaceWithSubscriptions, fragmentMatcher, - initialState: window.__SERVER_STATE__ && window.__SERVER_STATE__.apollo, - ssrForceFetchDelay: 100, - queryDeduplication: true, - dataIdFromObject: result => { - if (result.__typename) { - // Custom Community cache key based on slug - if (result.__typename === 'Community' && !!result.slug) { - return `${result.__typename}:${result.slug}`; - } - // Custom Channel cache key based on slug and community slug - if ( - result.__typename === 'Channel' && - !!result.slug && - !!result.community && - !!result.community.slug - ) { - return `${result.__typename}:${result.community.slug}:${result.slug}`; - } - // This was copied from the default dataIdFromObject - if (result.id !== undefined) { - return `${result.__typename}:${result.id}`; - } - if (result._id !== undefined) { - return `${result.__typename}:${result._id}`; - } - } - return null; - }, - customResolvers: { - Query: { - thread: (_, { id }) => - toIdValue(client.dataIdFromObject({ __typename: 'Thread', id })), - community: (_, { slug }) => - toIdValue(client.dataIdFromObject({ __typename: 'Community', slug })), - channel: (_, { channelSlug, communitySlug }) => - toIdValue( - client.dataIdFromObject({ - __typename: 'Channel', - slug: channelSlug, - community: { slug: communitySlug }, - }) - ), - }, + initialState: window.__SERVER_STATE__ && { + apollo: window.__SERVER_STATE__.apollo, }, + ssrForceFetchDelay: 100, + ...getSharedApolloClientOptions(), }); export const clearApolloStore = () => { From 17ca3861d2c09c90d09359a82fb4c0f1ee926433 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Sat, 2 Sep 2017 09:30:39 +0200 Subject: [PATCH 21/31] Convert shared options to ES5 --- shared/graphql/apollo-client-options.js | 106 ++++++++++++++---------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/shared/graphql/apollo-client-options.js b/shared/graphql/apollo-client-options.js index b8a726f789..5e57debd37 100644 --- a/shared/graphql/apollo-client-options.js +++ b/shared/graphql/apollo-client-options.js @@ -1,49 +1,67 @@ // @flow -import { toIdValue } from 'react-apollo'; +var apollo = require('react-apollo'), + toIdValue = apollo.toIdValue; -const getSharedApolloClientOptions = client => ({ - queryDeduplication: true, - dataIdFromObject: result => { - if (result.__typename) { - // Custom Community cache key based on slug - if (result.__typename === 'Community' && !!result.slug) { - return `${result.__typename}:${result.slug}`; +var getSharedApolloClientOptions = function getSharedApolloClientOptions( + client +) { + return { + queryDeduplication: true, + dataIdFromObject: function dataIdFromObject(result) { + if (result.__typename) { + // Custom Community cache key based on slug + if (result.__typename === 'Community' && !!result.slug) { + return result.__typename + ':' + result.slug; + } + // Custom Channel cache key based on slug and community slug + if ( + result.__typename === 'Channel' && + !!result.slug && + !!result.community && + !!result.community.slug + ) { + return ( + result.__typename + ':' + result.community.slug + ':' + result.slug + ); + } + // This was copied from the default dataIdFromObject + if (result.id !== undefined) { + return result.__typename + ':' + result.id; + } + if (result._id !== undefined) { + return result.__typename + ':' + result._id; + } } - // Custom Channel cache key based on slug and community slug - if ( - result.__typename === 'Channel' && - !!result.slug && - !!result.community && - !!result.community.slug - ) { - return `${result.__typename}:${result.community.slug}:${result.slug}`; - } - // This was copied from the default dataIdFromObject - if (result.id !== undefined) { - return `${result.__typename}:${result.id}`; - } - if (result._id !== undefined) { - return `${result.__typename}:${result._id}`; - } - } - return null; - }, - customResolvers: { - Query: { - thread: (_, { id }) => - toIdValue(client.dataIdFromObject({ __typename: 'Thread', id })), - community: (_, { slug }) => - toIdValue(client.dataIdFromObject({ __typename: 'Community', slug })), - channel: (_, { channelSlug, communitySlug }) => - toIdValue( - client.dataIdFromObject({ - __typename: 'Channel', - slug: channelSlug, - community: { slug: communitySlug }, - }) - ), + return null; + }, + customResolvers: { + Query: { + thread: function thread(_, _ref) { + var id = _ref.id; + return toIdValue( + client.dataIdFromObject({ __typename: 'Thread', id: id }) + ); + }, + community: function community(_, _ref2) { + var slug = _ref2.slug; + return toIdValue( + client.dataIdFromObject({ __typename: 'Community', slug: slug }) + ); + }, + channel: function channel(_, _ref3) { + var channelSlug = _ref3.channelSlug, + communitySlug = _ref3.communitySlug; + return toIdValue( + client.dataIdFromObject({ + __typename: 'Channel', + slug: channelSlug, + community: { slug: communitySlug }, + }) + ); + }, + }, }, - }, -}); + }; +}; -export default getSharedApolloClientOptions; +module.exports = getSharedApolloClientOptions; From f192bd132c214dad1893f0c31bfc913c8b70a30a Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Sat, 2 Sep 2017 15:55:41 +0200 Subject: [PATCH 22/31] Fix meta tags in SSR Duplicate meta tags are a big no-go. Since we're now server-side rendering we can simply remove the default meta tags from the index.html to get consistent meta tags everywhere. --- iris/renderer/get-html.js | 6 ++++-- public/index.html | 12 ++---------- src/components/head/index.js | 11 +++++++++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/iris/renderer/get-html.js b/iris/renderer/get-html.js index 9875a2f7a9..66583ce582 100644 --- a/iris/renderer/get-html.js +++ b/iris/renderer/get-html.js @@ -25,7 +25,9 @@ export const getHTML = ({ styleTags, metaTags, state, content }: Arguments) => { '
', `
${content}
` ) - // Inject the meta and style tags at the end of the - .replace('', `${metaTags}${styleTags}`) + // Inject the meta tags at the start of the + .replace('', `${metaTags}`) + // Inject the style tags at the end of the + .replace('', `${styleTags}`) ); }; diff --git a/public/index.html b/public/index.html index cbfd01239b..0cf70a726a 100644 --- a/public/index.html +++ b/public/index.html @@ -3,18 +3,11 @@ - - - - Spectrum - - - @@ -23,8 +16,6 @@ - - @@ -41,7 +32,8 @@ i[r] || function() { (i[r].q = i[r].q || []).push(arguments); - }), (i[r].l = 1 * new Date()); + }), + (i[r].l = 1 * new Date()); (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); a.async = 1; a.src = g; diff --git a/src/components/head/index.js b/src/components/head/index.js index edd7032d03..825e04f0e5 100644 --- a/src/components/head/index.js +++ b/src/components/head/index.js @@ -17,12 +17,19 @@ export default ({ title, description, showUnreadFavicon }: Props) => { - {showUnreadFavicon && + {showUnreadFavicon ? ( } + /> + ) : ( + + )} ); }; From eb0d44eef6b8861ee8c04b08d31fa2a1861256f7 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Sat, 2 Sep 2017 17:06:07 +0200 Subject: [PATCH 23/31] Sanitize injected state properly --- iris/renderer/get-html.js | 9 ++++----- package.json | 1 + yarn.lock | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/iris/renderer/get-html.js b/iris/renderer/get-html.js index 66583ce582..e873d916b7 100644 --- a/iris/renderer/get-html.js +++ b/iris/renderer/get-html.js @@ -1,6 +1,7 @@ // @flow import fs from 'fs'; import path from 'path'; +import serialize from 'serialize-javascript'; const html = fs .readFileSync(path.resolve(__dirname, '..', '..', 'build', 'index.html')) @@ -14,16 +15,14 @@ type Arguments = { }; export const getHTML = ({ styleTags, metaTags, state, content }: Arguments) => { - // TODO: Proper sanitization - // NOTE(@mxstbr): There's some library by Yahoo (I think) - // specifically for this purpose - const sanitizedState = JSON.stringify(state).replace(/ .replace( '
', - `
${content}
` + `
${content}
` ) // Inject the meta tags at the start of the .replace('', `${metaTags}`) diff --git a/package.json b/package.json index e7e7329b40..22be16ec92 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "rethinkdb-migrate": "^1.1.0", "rethinkdbdash": "^2.3.29", "s3-image-uploader": "^1.0.7", + "serialize-javascript": "^1.4.0", "session-rethinkdb": "^2.0.0", "slate": "^0.20.1", "slate-markdown": "0.1.0", diff --git a/yarn.lock b/yarn.lock index 5e112a1a54..603f93a69f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7656,6 +7656,10 @@ send@0.15.4: range-parser "~1.2.0" statuses "~1.3.1" +serialize-javascript@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" + serve-index@^1.7.2: version "1.9.0" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7" From fd85845bf36bc9d884dbc5616e09cee4a7961e5a Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Mon, 4 Sep 2017 19:26:44 +0200 Subject: [PATCH 24/31] Fix thread loading --- shared/graphql/apollo-client-options.js | 68 +++++++++---------- src/components/profile/thread.js | 7 +- src/components/threadFeedCard/index.js | 14 +++- src/helpers/notifications.js | 9 ++- .../components/newMessageNotification.js | 14 +++- .../components/newReactionNotification.js | 10 ++- src/views/notifications/utils.js | 7 +- 7 files changed, 85 insertions(+), 44 deletions(-) diff --git a/shared/graphql/apollo-client-options.js b/shared/graphql/apollo-client-options.js index 5e57debd37..ea01f4c6a8 100644 --- a/shared/graphql/apollo-client-options.js +++ b/shared/graphql/apollo-client-options.js @@ -2,57 +2,55 @@ var apollo = require('react-apollo'), toIdValue = apollo.toIdValue; -var getSharedApolloClientOptions = function getSharedApolloClientOptions( - client -) { +function dataIdFromObject(result) { + if (result.__typename) { + // Custom Community cache key based on slug + if (result.__typename === 'Community' && !!result.slug) { + return result.__typename + ':' + result.slug; + } + // Custom Channel cache key based on slug and community slug + if ( + result.__typename === 'Channel' && + !!result.slug && + !!result.community && + !!result.community.slug + ) { + return ( + result.__typename + ':' + result.community.slug + ':' + result.slug + ); + } + // This was copied from the default dataIdFromObject + if (result.id !== undefined) { + return result.__typename + ':' + result.id; + } + if (result._id !== undefined) { + return result.__typename + ':' + result._id; + } + } + return null; +} + +var getSharedApolloClientOptions = function getSharedApolloClientOptions() { return { queryDeduplication: true, - dataIdFromObject: function dataIdFromObject(result) { - if (result.__typename) { - // Custom Community cache key based on slug - if (result.__typename === 'Community' && !!result.slug) { - return result.__typename + ':' + result.slug; - } - // Custom Channel cache key based on slug and community slug - if ( - result.__typename === 'Channel' && - !!result.slug && - !!result.community && - !!result.community.slug - ) { - return ( - result.__typename + ':' + result.community.slug + ':' + result.slug - ); - } - // This was copied from the default dataIdFromObject - if (result.id !== undefined) { - return result.__typename + ':' + result.id; - } - if (result._id !== undefined) { - return result.__typename + ':' + result._id; - } - } - return null; - }, + dataIdFromObject: dataIdFromObject, customResolvers: { Query: { thread: function thread(_, _ref) { var id = _ref.id; - return toIdValue( - client.dataIdFromObject({ __typename: 'Thread', id: id }) - ); + return toIdValue(dataIdFromObject({ __typename: 'Thread', id: id })); }, community: function community(_, _ref2) { var slug = _ref2.slug; return toIdValue( - client.dataIdFromObject({ __typename: 'Community', slug: slug }) + dataIdFromObject({ __typename: 'Community', slug: slug }) ); }, channel: function channel(_, _ref3) { var channelSlug = _ref3.channelSlug, communitySlug = _ref3.communitySlug; return toIdValue( - client.dataIdFromObject({ + dataIdFromObject({ __typename: 'Channel', slug: channelSlug, community: { slug: communitySlug }, diff --git a/src/components/profile/thread.js b/src/components/profile/thread.js index b5d3526256..1b23028a01 100644 --- a/src/components/profile/thread.js +++ b/src/components/profile/thread.js @@ -19,7 +19,12 @@ class ThreadWithData extends Component { } return ( - + => { return ( - + - + {props.data.content.title} {props.isPinned && ( diff --git a/src/helpers/notifications.js b/src/helpers/notifications.js index f44afe8cb7..04dbb1009b 100644 --- a/src/helpers/notifications.js +++ b/src/helpers/notifications.js @@ -45,7 +45,14 @@ export const constructMessage = notification => { return ( {sender.name} replied to your{' '} - thread: + + thread + : ); default: diff --git a/src/views/notifications/components/newMessageNotification.js b/src/views/notifications/components/newMessageNotification.js index 7786fe9106..76351bfcc6 100644 --- a/src/views/notifications/components/newMessageNotification.js +++ b/src/views/notifications/components/newMessageNotification.js @@ -82,7 +82,12 @@ export const NewMessageNotification = ({ notification, currentUser }) => { return ( - + @@ -193,7 +198,12 @@ export const MiniNewMessageNotification = ({ return ( - + diff --git a/src/views/notifications/components/newReactionNotification.js b/src/views/notifications/components/newReactionNotification.js index c9c385822d..6d718a73dd 100644 --- a/src/views/notifications/components/newReactionNotification.js +++ b/src/views/notifications/components/newReactionNotification.js @@ -41,7 +41,10 @@ export const NewReactionNotification = ({ notification, currentUser }) => { return ( @@ -141,7 +144,10 @@ export const MiniNewReactionNotification = ({ return ( diff --git a/src/views/notifications/utils.js b/src/views/notifications/utils.js index d013b0f1b3..cce8a93ea5 100644 --- a/src/views/notifications/utils.js +++ b/src/views/notifications/utils.js @@ -173,7 +173,12 @@ const threadToString = (context, currentUser) => { {' '} {str}{' '} - + {context.payload.content.title} From fb6b421d27cfe30a34f9a35bc37d55dcecfefa46 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 7 Sep 2017 20:35:19 +0200 Subject: [PATCH 25/31] Enable ssr in dev by default --- iris/index.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/iris/index.js b/iris/index.js index bc73f87892..876cf22513 100644 --- a/iris/index.js +++ b/iris/index.js @@ -39,18 +39,12 @@ app.use('/api', apiRoutes); import stripeRoutes from './routes/stripe'; app.use('/stripe', stripeRoutes); -// In production use express to server-side render the React app -// In development we don't server-side render to get live reloading etc. -if (IS_PROD || process.env.DEV_SSR) { - console.log('Enabled server-side rendering'); - const renderer = require('./renderer').default; - app.use( - express.static(path.resolve(__dirname, '..', 'build'), { index: false }) - ); - app.get('*', renderer); -} else { - console.log('Server-side rendering disabled for development'); -} +// Use express to server-side render the React app +const renderer = require('./renderer').default; +app.use( + express.static(path.resolve(__dirname, '..', 'build'), { index: false }) +); +app.get('*', renderer); import type { Loader } from './loaders/types'; export type GraphQLContext = { @@ -71,4 +65,7 @@ server.listen(PORT); // Start database listeners listeners.start(); -console.log(`GraphQL server running at port ${PORT}!`); +console.log(`GraphQL server running at http://localhost:${PORT}/api`); +console.log( + `Web server running at http://localhost:${PORT}, server-side rendering enabled` +); From 1c274ab638b37979e69bb6441bb4c4bee09fca1c Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 7 Sep 2017 20:42:21 +0200 Subject: [PATCH 26/31] Add docs about developing SSR --- docs/backend/iris/server-side-rendering.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/backend/iris/server-side-rendering.md diff --git a/docs/backend/iris/server-side-rendering.md b/docs/backend/iris/server-side-rendering.md new file mode 100644 index 0000000000..fb5b7ab0d6 --- /dev/null +++ b/docs/backend/iris/server-side-rendering.md @@ -0,0 +1,18 @@ +# Server-side rendering + +In production we server our React-based frontend (`src/`) server-side rendered, meaning we do an initial render on the server and send down static HTML, then rehydrate with the JS bundle. + +## Developing SSR + +When you develop Spectrum you're usually running two processes, `yarn run dev:client` for the frontend and `yarn run dev:server` for Iris, the GraphQL API. This means in your browser you access `localhost:3000`, which is a fully client-side React app, and that then fetches data from `localhost:3001/api`. + +To test server-side rendering locally just load Spectrum from `localhost:3001` (instead of `:3000`), which will request the HTML from Iris rather than `webpack-dev-server`. + +The upside of this setup is that we get the best development experience locally with hot module reloading etc, and in production we only have one SSR process. + +The only downside is that when you're testing SSR and changing the frontend those changes won't be reflected. To get changes from the frontend reflected when you're requesting `localhost:3001` you have to: + +1. Stop the `yarn run dev:client` process +2. Run `yarn run dev:client` +3. Wait for the first compilation to complete +4. Restart the `yarn run dev:server` process by stopping and then starting it again From 0fa3f74a57f8c253c4bfd56d0cd388258291a938 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 7 Sep 2017 20:44:31 +0200 Subject: [PATCH 27/31] Fix .babelrc --- .babelrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.babelrc b/.babelrc index 13a1e631e1..dcc41f2564 100644 --- a/.babelrc +++ b/.babelrc @@ -15,7 +15,7 @@ ] ], "plugins": [ - ["styled-components", { "ssr": true }] + ["styled-components", { "ssr": true }], "transform-flow-strip-types", "transform-object-rest-spread", "babel-plugin-transform-react-jsx", From e55abe8147e35b38ba52e936a5a39c7d239c736f Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Thu, 7 Sep 2017 11:52:26 -0700 Subject: [PATCH 28/31] Update server-side-rendering.md --- docs/backend/iris/server-side-rendering.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/backend/iris/server-side-rendering.md b/docs/backend/iris/server-side-rendering.md index fb5b7ab0d6..e3e99cec26 100644 --- a/docs/backend/iris/server-side-rendering.md +++ b/docs/backend/iris/server-side-rendering.md @@ -6,6 +6,8 @@ In production we server our React-based frontend (`src/`) server-side rendered, When you develop Spectrum you're usually running two processes, `yarn run dev:client` for the frontend and `yarn run dev:server` for Iris, the GraphQL API. This means in your browser you access `localhost:3000`, which is a fully client-side React app, and that then fetches data from `localhost:3001/api`. + +## When developing SSR features directly: To test server-side rendering locally just load Spectrum from `localhost:3001` (instead of `:3000`), which will request the HTML from Iris rather than `webpack-dev-server`. The upside of this setup is that we get the best development experience locally with hot module reloading etc, and in production we only have one SSR process. @@ -16,3 +18,6 @@ The only downside is that when you're testing SSR and changing the frontend thos 2. Run `yarn run dev:client` 3. Wait for the first compilation to complete 4. Restart the `yarn run dev:server` process by stopping and then starting it again + +## When doing all other client and Iris development: +Just stick to the normal workflow of `yarn run dev:client` and enjoy the hot reloading at `localhost:3000`! From 11d04dcdb51cb68c6b8448f43b44c837811e88d4 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 7 Sep 2017 21:21:12 +0200 Subject: [PATCH 29/31] Fix babel --- .babelrc | 1 + package.json | 1 + yarn.lock | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.babelrc b/.babelrc index dcc41f2564..efc55191ee 100644 --- a/.babelrc +++ b/.babelrc @@ -15,6 +15,7 @@ ] ], "plugins": [ + "babel-plugin-transform-class-properties", ["styled-components", { "ssr": true }], "transform-flow-strip-types", "transform-object-rest-spread", diff --git a/package.json b/package.json index 22be16ec92..bfae4b628d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "devDependencies": { "babel-cli": "^6.24.1", + "babel-plugin-transform-class-properties": "^6.24.1", "cross-env": "^5.0.5", "flow-bin": "^0.43.0", "lint-staged": "^3.3.0", diff --git a/yarn.lock b/yarn.lock index 603f93a69f..1d4b513107 100644 --- a/yarn.lock +++ b/yarn.lock @@ -796,7 +796,7 @@ babel-plugin-transform-async-to-generator@^6.22.0: babel-plugin-syntax-async-functions "^6.8.0" babel-runtime "^6.22.0" -babel-plugin-transform-class-properties@6.24.1, babel-plugin-transform-class-properties@^6.19.0: +babel-plugin-transform-class-properties@6.24.1, babel-plugin-transform-class-properties@^6.19.0, babel-plugin-transform-class-properties@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" dependencies: From 761d1a5ee1ca254fa50489e89b0ebbb92d9e9542 Mon Sep 17 00:00:00 2001 From: Maximilian Stoiber Date: Thu, 7 Sep 2017 21:24:47 +0200 Subject: [PATCH 30/31] Add meta tags to notifications --- src/views/notifications/index.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/views/notifications/index.js b/src/views/notifications/index.js index 680d48764b..b8f172281e 100644 --- a/src/views/notifications/index.js +++ b/src/views/notifications/index.js @@ -19,6 +19,7 @@ import { CommunityInviteNotification } from './components/communityInviteNotific import { NewUserInCommunityNotification } from './components/newUserInCommunityNotification'; import { Column } from '../../components/column'; import AppViewWrapper from '../../components/appViewWrapper'; +import Head from '../../components/head'; import Titlebar from '../../views/titlebar'; import { displayLoadingNotifications, @@ -45,6 +46,7 @@ import { UpsellNullNotifications, } from '../../components/upsell'; import BrowserNotificationRequest from './components/browserNotificationRequest'; +import generateMetaInfo from 'shared/generate-meta-info'; class NotificationsPure extends Component { state: { @@ -175,10 +177,15 @@ class NotificationsPure extends Component { ); } + const { title, description } = generateMetaInfo({ + type: 'notifications', + }); + if (!data.notifications || data.notifications.edges.length === 0) { return ( + @@ -198,15 +205,17 @@ class NotificationsPure extends Component { return ( + - {this.state.showWebPushPrompt && + {this.state.showWebPushPrompt && ( } + /> + )} Date: Thu, 7 Sep 2017 12:50:27 -0700 Subject: [PATCH 31/31] Remove unused mutation file --- iris/mutations/invoice.js | 97 --------------------------------------- 1 file changed, 97 deletions(-) delete mode 100644 iris/mutations/invoice.js diff --git a/iris/mutations/invoice.js b/iris/mutations/invoice.js deleted file mode 100644 index bd4bfea070..0000000000 --- a/iris/mutations/invoice.js +++ /dev/null @@ -1,97 +0,0 @@ -// @flow -// $FlowFixMe -import UserError from '../utils/UserError'; -// $FlowFixMe -const env = require('node-env-file'); -const IS_PROD = process.env.NODE_ENV === 'production'; -// $FlowFixMe -const path = require('path'); -env(path.resolve(__dirname, '../.env'), { raise: false }); -const STRIPE_TOKEN = process.env.STRIPE_TOKEN; -const stripe = require('stripe')(STRIPE_TOKEN), - currency = 'USD'; - -import { payInvoice, getInvoice } from '../models/invoice'; - -const parseStripeErrors = err => { - switch (err.type) { - case 'StripeCardError': - // A declined card error - return new UserError(err); // => e.g. "Your card's expiration year is invalid." - break; - case 'RateLimitError': - // Too many requests made to the API too quickly - return new UserError( - 'Could not pay this invoice at this time, try again later' - ); - break; - case 'StripeInvalidRequestError': - // Invalid parameters were supplied to Stripe's API - return new UserError( - 'Could not pay this invoice at this time, try again later' - ); - break; - case 'StripeAPIError': - // An error occurred internally with Stripe's API - return new UserError('Something went wrong at Stripe, try again later'); - break; - case 'StripeConnectionError': - // Some kind of error occurred during the HTTPS communication - return new UserError('Something went wrong at Stripe, try again later'); - break; - case 'StripeAuthenticationError': - // You probably used an incorrect API key - return new UserError('Something went wrong at Stripe, try again later'); - break; - default: - // Handle any other types of unexpected errors - return new UserError('Something went wrong, try again later'); - break; - } -}; - -module.exports = { - Mutation: { - payInvoice: (_, { input }, { user }) => { - const currentUser = user; - - // user must be authed to create a community - if (!currentUser) { - return new UserError('You must be signed in to pay an invoice.'); - } - - // gql should have caught this, but just in case not token or plan - // was specified, return an error - if (!input.id || !input.token) { - return new UserError( - "We aren't able to process this invoice right now. Try again soon." - ); - } - - // parse the token string into an object - let token = JSON.parse(input.token); - - return getInvoice(input.id).then(invoice => { - if (invoice.paidAt || invoice.stripeData) { - return new UserError('This invoice has already been paid.'); - } - - return ( - stripe.charges - .create({ - amount: invoice.amount, - currency: 'usd', - source: token.id, - description: invoice.note, - }) - // creat the recurringPayment object - .then(charge => payInvoice(invoice.id, charge)) - .catch(err => { - console.log(err, err.message); - return parseStripeErrors(err); - }) - ); - }); - }, - }, -};