From 118301c2b4484bca9d5f2e20621b47ff10fc3af4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Feb 2018 12:25:51 +0100 Subject: [PATCH 1/8] Implement a session storage --- client/coral-embed/src/Stream.js | 15 +++++++++-- client/coral-framework/services/storage.js | 31 +++++++++++++++------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/client/coral-embed/src/Stream.js b/client/coral-embed/src/Stream.js index b3258c271..fc8c0d5e7 100644 --- a/client/coral-embed/src/Stream.js +++ b/client/coral-embed/src/Stream.js @@ -146,8 +146,19 @@ export default class Stream { // If the user clicks outside the embed, then tell the embed. document.addEventListener('click', this.handleClick.bind(this), true); - // Listens to storage requests on pym and relay it to local storage. - connectStorageToPym(createStorage(), this.pym); + // Listens to local storage requests on pym and relay it to local storage. + connectStorageToPym( + createStorage('localStorage'), + this.pym, + 'localStorage' + ); + + // Listens to session storage requests on pym and relay it to session storage. + connectStorageToPym( + createStorage('sessionStorage'), + this.pym, + 'sessionStorage' + ); } login(token) { diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js index 1909373b7..de96fdf4c 100644 --- a/client/coral-framework/services/storage.js +++ b/client/coral-framework/services/storage.js @@ -34,8 +34,8 @@ function getStorage(type) { * createStorage returns a localStorage wrapper if available * @return {Object} localStorage wrapper */ -export function createStorage() { - return getStorage('localStorage'); +export function createStorage(type = 'localStorage') { + return getStorage(type); } /** @@ -44,7 +44,7 @@ export function createStorage() { * @param {string} pym pym * @return {Object} storage */ -export function createPymStorage(pym) { +export function createPymStorage(pym, type = 'localStorage') { // A Map of requestID => {resolve, reject} const requests = {}; @@ -54,21 +54,21 @@ export function createPymStorage(pym) { return new Promise((resolve, reject) => { requests[id] = { resolve, reject }; pym.sendMessage( - 'pymStorage.request', + `pymStorage.${type}.request`, JSON.stringify({ id, method, parameters }) ); }); }; // Receive successful responses. - pym.onMessage('pymStorage.response', msg => { + pym.onMessage(`pymStorage.${type}.response`, msg => { const { id, result } = JSON.parse(msg); requests[id].resolve(result); delete requests[id]; }); // Receive error responses. - pym.onMessage('pymStorage.error', msg => { + pym.onMessage(`pymStorage.${type}.error`, msg => { const { id, error } = JSON.parse(msg); requests[id].reject(error); delete requests[id]; @@ -88,8 +88,13 @@ export function createPymStorage(pym) { * @param {Object} pym pym to listen to storage requests * @param {string} prefix namespace requests by prepending a prefix to the keys */ -export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') { - pym.onMessage('pymStorage.request', msg => { +export function connectStorageToPym( + storage, + pym, + type = 'localStorage', + prefix = 'talkPymStorage:' +) { + pym.onMessage(`pymStorage.${type}.request`, msg => { const { id, method, parameters } = JSON.parse(msg); const { key, value } = parameters; const prefixedKey = `${prefix}${key}`; @@ -99,7 +104,10 @@ export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') { const sendError = error => { console.error(error); - pym.sendMessage('pymStorage.error', JSON.stringify({ id, error })); + pym.sendMessage( + `pymStorage.${type}.error`, + JSON.stringify({ id, error }) + ); }; try { @@ -122,6 +130,9 @@ export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') { return; } - pym.sendMessage('pymStorage.response', JSON.stringify({ id, result })); + pym.sendMessage( + `pymStorage.${type}.response`, + JSON.stringify({ id, result }) + ); }); } From 91e08cb117bae44fd6d96e226a68da6ed15ab433 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Feb 2018 13:28:28 +0100 Subject: [PATCH 2/8] Rename storage to localStorage in context --- client/coral-admin/src/actions/auth.js | 30 +++++++++++-------- client/coral-admin/src/actions/moderation.js | 6 ++-- client/coral-admin/src/index.js | 4 +-- client/coral-embed-stream/src/actions/auth.js | 24 +++++++-------- .../components/TalkProvider.js | 4 +-- client/coral-framework/services/bootstrap.js | 10 +++---- client/coral-framework/services/i18n.js | 2 +- .../talk-plugin-remember-sort/client/index.js | 8 ++--- 8 files changed, 46 insertions(+), 42 deletions(-) diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 332d756c0..3304da85f 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -10,7 +10,7 @@ import jwtDecode from 'jwt-decode'; export const handleLogin = (email, password, recaptchaResponse) => ( dispatch, _, - { rest, client, storage } + { rest, client, localStorage } ) => { dispatch({ type: actions.LOGIN_REQUEST }); @@ -31,8 +31,8 @@ export const handleLogin = (email, password, recaptchaResponse) => ( return rest('/auth/local', params) .then(({ user, token }) => { if (!user) { - if (!bowser.safari && !bowser.ios && storage) { - storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && localStorage) { + localStorage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -121,13 +121,17 @@ const checkLoginFailure = error => ({ error, }); -export const checkLogin = () => (dispatch, _, { rest, client, storage }) => { +export const checkLogin = () => ( + dispatch, + _, + { rest, client, localStorage } +) => { dispatch(checkLoginRequest()); return rest('/auth') .then(({ user }) => { if (!user) { - if (!bowser.safari && !bowser.ios && storage) { - storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && localStorage) { + localStorage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -148,10 +152,10 @@ export const checkLogin = () => (dispatch, _, { rest, client, storage }) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch, _, { rest, client, storage }) => { +export const logout = () => (dispatch, _, { rest, client, localStorage }) => { return rest('/auth', { method: 'DELETE' }).then(() => { - if (storage) { - storage.removeItem('token'); + if (localStorage) { + localStorage.removeItem('token'); } // Reset the websocket. @@ -165,10 +169,10 @@ export const logout = () => (dispatch, _, { rest, client, storage }) => { // AUTH TOKEN //============================================================================== -export const handleAuthToken = token => (dispatch, _, { storage }) => { - if (storage) { - storage.setItem('exp', jwtDecode(token).exp); - storage.setItem('token', token); +export const handleAuthToken = token => (dispatch, _, { localStorage }) => { + if (localStorage) { + localStorage.setItem('exp', jwtDecode(token).exp); + localStorage.setItem('token', token); } dispatch({ type: 'HANDLE_AUTH_TOKEN' }); }; diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 7899b7685..16c2bd2f7 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -4,10 +4,10 @@ export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open }); export const singleView = () => ({ type: actions.SINGLE_VIEW }); // hide shortcuts note -export const hideShortcutsNote = () => (dispatch, _, { storage }) => { +export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => { try { - if (storage) { - storage.setItem('coral:shortcutsNote', 'hide'); + if (localStorage) { + localStorage.setItem('coral:shortcutsNote', 'hide'); } } catch (e) { // above will fail in Safari private mode diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index ab9ee32b3..0cf0a8cf9 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -14,8 +14,8 @@ import { hideShortcutsNote } from './actions/moderation'; smoothscroll.polyfill(); -function init({ store, storage }) { - if (storage && storage.getItem('coral:shortcutsNote') === 'hide') { +function init({ store, localStorage }) { + if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') { store.dispatch(hideShortcutsNote()); } } diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index e74265026..ac4b96c2c 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -90,10 +90,10 @@ const signInFailure = error => ({ // AUTH TOKEN //============================================================================== -export const handleAuthToken = token => (dispatch, _, { storage }) => { - if (storage) { - storage.setItem('exp', jwtDecode(token).exp); - storage.setItem('token', token); +export const handleAuthToken = token => (dispatch, _, { localStorage }) => { + if (localStorage) { + localStorage.setItem('exp', jwtDecode(token).exp); + localStorage.setItem('token', token); } dispatch({ type: 'HANDLE_AUTH_TOKEN' }); @@ -260,12 +260,12 @@ export const fetchForgotPassword = email => (dispatch, getState, { rest }) => { export const logout = () => async ( dispatch, _, - { rest, client, pym, storage } + { rest, client, pym, localStorage } ) => { await rest('/auth', { method: 'DELETE' }); - if (storage) { - storage.removeItem('token'); + if (localStorage) { + localStorage.removeItem('token'); } // Reset the websocket. @@ -296,14 +296,14 @@ const ErrNotLoggedIn = new Error('Not logged in'); export const checkLogin = () => ( dispatch, _, - { rest, client, pym, storage } + { rest, client, pym, localStorage } ) => { dispatch(checkLoginRequest()); rest('/auth') .then(result => { if (!result.user) { - if (storage) { - storage.removeItem('token'); + if (localStorage) { + localStorage.removeItem('token'); } throw ErrNotLoggedIn; } @@ -326,9 +326,9 @@ export const checkLogin = () => ( if (error !== ErrNotLoggedIn) { console.error(error); } - if (error.status && error.status === 401 && storage) { + if (error.status && error.status === 401 && localStorage) { // Unauthorized. - storage.removeItem('token'); + localStorage.removeItem('token'); } const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 73dd4d86d..b9654f688 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -11,7 +11,7 @@ class TalkProvider extends React.Component { rest: this.props.rest, graphql: this.props.graphql, notification: this.props.notification, - storage: this.props.storage, + localStorage: this.props.localStorage, history: this.props.history, store: this.props.store, }; @@ -30,7 +30,7 @@ TalkProvider.childContextTypes = { rest: PropTypes.func, graphql: PropTypes.object, notification: PropTypes.object, - storage: PropTypes.object, + localStorage: PropTypes.object, history: PropTypes.object, store: PropTypes.object, }; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 66da3ff29..d784cb08c 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -64,8 +64,8 @@ export async function createContext({ init = noop, } = {}) { const eventEmitter = new EventEmitter({ wildcard: true }); - const storage = createStorage(); - const pymStorage = createPymStorage(pym); + const localStorage = createStorage('localStorage'); + const pymLocalStorage = createPymStorage(pym, 'localStorage'); const history = createHistory(BASE_PATH); const introspection = createIntrospection(introspectionData); let store = null; @@ -75,7 +75,7 @@ export async function createContext({ // 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, storage); + return getAuthToken(store, localStorage); }; const rest = createRestClient({ @@ -116,10 +116,10 @@ export async function createContext({ rest, graphql, notification, - storage, + localStorage, history, introspection, - pymStorage, + pymLocalStorage, }; // Load framework fragments. diff --git a/client/coral-framework/services/i18n.js b/client/coral-framework/services/i18n.js index ee7ecc266..1ec53c703 100644 --- a/client/coral-framework/services/i18n.js +++ b/client/coral-framework/services/i18n.js @@ -68,7 +68,7 @@ function getLocale(storage) { export function setupTranslations() { // Setup the translation framework with the storage. - const storage = createStorage(); + const storage = createStorage('localStorage'); const locale = getLocale(storage); setLocale(storage, locale); diff --git a/plugins/talk-plugin-remember-sort/client/index.js b/plugins/talk-plugin-remember-sort/client/index.js index 1309a3dc3..a4fb7424c 100644 --- a/plugins/talk-plugin-remember-sort/client/index.js +++ b/plugins/talk-plugin-remember-sort/client/index.js @@ -7,7 +7,7 @@ import { const STORAGE_PATH = 'talkPluginRememberSort'; export default { - init: async ({ store, pymStorage, introspection }) => { + init: async ({ store, pymLocalStorage, introspection }) => { // TODO: workaround as this plugin is included in any target and // embeds (e.g. admin), but should only be included inside the stream. @@ -16,10 +16,10 @@ export default { return; } - // We use pymStorage instead to persist the data directly on the parent page, + // We use pymLocalStorage instead to persist the data directly on the parent page, // in order to mitigate strict cross domain security settings. - let sort = JSON.parse(await pymStorage.getItem(STORAGE_PATH)); + let sort = JSON.parse(await pymLocalStorage.getItem(STORAGE_PATH)); if ( sort && introspection.isValidEnumValue('SORT_ORDER', sort.sortOrder) && @@ -35,7 +35,7 @@ export default { // Save sorting choice to storage if it has changed. if (!sort || sort.sortOrder !== sortOrder || sort.sortBy !== sortBy) { sort = { sortOrder, sortBy }; - pymStorage.setItem(STORAGE_PATH, JSON.stringify(sort)); + pymLocalStorage.setItem(STORAGE_PATH, JSON.stringify(sort)); } }); }, From 2e59f64738fe4f609c8c808038c0448aa71c6bc2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Feb 2018 13:34:03 +0100 Subject: [PATCH 3/8] Pass sessionStorage to context --- client/coral-framework/components/TalkProvider.js | 8 ++++++++ client/coral-framework/services/bootstrap.js | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index b9654f688..3164d8e9d 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -12,8 +12,11 @@ class TalkProvider extends React.Component { graphql: this.props.graphql, notification: this.props.notification, localStorage: this.props.localStorage, + sessionStorage: this.props.sessionStorage, history: this.props.history, store: this.props.store, + pymLocalStorage: this.props.pymLocalStorage, + pymSessionStorage: this.props.pymSessionStorage, }; } @@ -31,8 +34,13 @@ TalkProvider.childContextTypes = { graphql: PropTypes.object, notification: PropTypes.object, localStorage: PropTypes.object, + sessionStorage: PropTypes.object, + pymLocalStorage: PropTypes.object, + pymSessionStorage: PropTypes.object, history: PropTypes.object, store: PropTypes.object, }; +TalkProvider.propTypes = TalkProvider.childContextTypes; + export default TalkProvider; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index d784cb08c..416666e89 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -65,7 +65,9 @@ export async function createContext({ } = {}) { const eventEmitter = new EventEmitter({ wildcard: true }); const localStorage = createStorage('localStorage'); + const sessionStorage = createStorage('sessionStorage'); const pymLocalStorage = createPymStorage(pym, 'localStorage'); + const pymSessionStorage = createPymStorage(pym, 'sessionStorage'); const history = createHistory(BASE_PATH); const introspection = createIntrospection(introspectionData); let store = null; @@ -117,9 +119,11 @@ export async function createContext({ graphql, notification, localStorage, + sessionStorage, history, introspection, pymLocalStorage, + pymSessionStorage, }; // Load framework fragments. From dc5a3e43a6fd9acd1d36338463abc1bbb6fc2756 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Feb 2018 13:53:15 +0100 Subject: [PATCH 4/8] If we are not in iFrame just use localStorage --- client/coral-embed-stream/src/index.js | 10 +--------- client/coral-framework/services/bootstrap.js | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 2226f46ab..45fc4c034 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -14,16 +14,8 @@ import reducers from './reducers'; import TalkProvider from 'coral-framework/components/TalkProvider'; import pluginsConfig from 'pluginsConfig'; -function inIframe() { - try { - return window.self !== window.top; - } catch (e) { - return true; - } -} - // TODO: move init code into `bootstrap` service after auth has been refactored. -function preInit({ store, pym }) { +function preInit({ store, pym, inIframe }) { // TODO: This is popup specific code and needs to be refactored. if (!inIframe()) { store.dispatch(addExternalConfig({})); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 416666e89..4d811a870 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -43,6 +43,14 @@ const getAuthToken = (store, storage) => { return null; }; +function areWeInIframe() { + try { + return window.self !== window.top; + } catch (e) { + return true; + } +} + /** * createContext setups and returns Talk dependencies that should be * passed to `TalkProvider`. @@ -63,11 +71,16 @@ export async function createContext({ preInit, init = noop, } = {}) { + const inIframe = areWeInIframe(); const eventEmitter = new EventEmitter({ wildcard: true }); const localStorage = createStorage('localStorage'); const sessionStorage = createStorage('sessionStorage'); - const pymLocalStorage = createPymStorage(pym, 'localStorage'); - const pymSessionStorage = createPymStorage(pym, 'sessionStorage'); + const pymLocalStorage = inIframe + ? createPymStorage(pym, 'localStorage') + : localStorage; + const pymSessionStorage = inIframe + ? createPymStorage(pym, 'sessionStorage') + : localStorage; const history = createHistory(BASE_PATH); const introspection = createIntrospection(introspectionData); let store = null; @@ -124,6 +137,7 @@ export async function createContext({ introspection, pymLocalStorage, pymSessionStorage, + inIframe, }; // Load framework fragments. From 34726e2c4e5e5666ed2edfd85ec4fcc78ea79700 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Feb 2018 17:29:53 +0100 Subject: [PATCH 5/8] Implement DraftArea --- client/coral-embed-stream/src/index.js | 2 +- .../src/tabs/stream/components/CommentForm.js | 47 +++-------- .../src/tabs/stream/components/DraftArea.js | 77 +++++++++++++++++++ .../components/EditableCommentContent.js | 3 + .../src/tabs/stream/components/ReplyBox.js | 5 +- .../src/tabs/stream/containers/CommentBox.js | 10 ++- .../src/tabs/stream/containers/DraftArea.js | 63 +++++++++++++++ client/coral-framework/services/bootstrap.js | 2 +- 8 files changed, 167 insertions(+), 42 deletions(-) create mode 100644 client/coral-embed-stream/src/tabs/stream/components/DraftArea.js create mode 100644 client/coral-embed-stream/src/tabs/stream/containers/DraftArea.js diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 45fc4c034..7100c0d28 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -17,7 +17,7 @@ import pluginsConfig from 'pluginsConfig'; // TODO: move init code into `bootstrap` service after auth has been refactored. function preInit({ store, pym, inIframe }) { // TODO: This is popup specific code and needs to be refactored. - if (!inIframe()) { + if (!inIframe) { store.dispatch(addExternalConfig({})); store.dispatch(checkLogin()); return; diff --git a/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js b/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js index 9a84bdbe8..28b8bb668 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js +++ b/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js @@ -2,13 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from 'coral-ui'; import cn from 'classnames'; -import Slot from 'coral-framework/components/Slot'; // TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest. import { name } from '../containers/CommentBox'; import styles from './CommentForm.css'; import t from 'coral-framework/services/i18n'; +import DraftArea from '../containers/DraftArea'; /** * Common UI for Creating or Editing a Comment @@ -61,10 +61,6 @@ export class CommentForm extends React.Component { }; } - onBodyChange = e => { - this.props.onBodyChange(e.target.value); - }; - onClickSubmit = () => { this.props.onSubmit(); }; @@ -107,37 +103,16 @@ export class CommentForm extends React.Component { return (
-
- -