diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index fee6fe025..a069984dc 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -6,6 +6,8 @@ import t from 'coral-framework/services/i18n'; import {TabBar, Tab, TabContent, TabPane} from 'coral-ui'; import ProfileContainer from 'coral-settings/containers/ProfileContainer'; +import Popup from 'coral-framework/components/Popup'; +import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty'; import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; import cn from 'classnames'; @@ -26,12 +28,24 @@ export default class Embed extends React.Component { }; render() { - const {activeTab, commentId} = this.props; + const {activeTab, commentId, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props; const {user} = this.props.auth; const hasHighlightedComment = !!commentId; return (
+ + +
- {getSlotComponents('streamTabs').map((PluginComponent) => ( + {getSlotComponents('streamTabs', pluginConfig, streamTabProps).map((PluginComponent) => ( ))} @@ -222,12 +222,10 @@ class Stream extends React.Component { - {getSlotComponents('streamTabPanes').map((PluginComponent) => ( + {getSlotComponents('streamTabPanes', pluginConfig, streamTabProps).map((PluginComponent) => ( ))} diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index a5c6ba1a6..aa0700ad5 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -19,7 +19,7 @@ import t from 'coral-framework/services/i18n'; import {setActiveTab} from '../actions/embed'; -const {logout, checkLogin} = authActions; +const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions; const {fetchAssetSuccess} = assetActions; class EmbedContainer extends React.Component { @@ -185,6 +185,9 @@ const mapDispatchToProps = (dispatch) => setActiveTab, fetchAssetSuccess, addNotification, + focusSignInDialog, + blurSignInDialog, + hideSignInDialog, }, dispatch ); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 994d749f3..4b4a0e105 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -310,7 +310,8 @@ const mapStateToProps = (state) => ({ previousTab: state.embed.previousTab, activeStreamTab: state.stream.activeTab, previousStreamTab: state.stream.previousTab, - commentClassNames: state.stream.commentClassNames + commentClassNames: state.stream.commentClassNames, + pluginConfig: state.config.plugin_config, }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 732a11ebe..d3c29dc3c 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -7,45 +7,33 @@ import pym from '../services/pym'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; -import {isSlotEmpty} from 'plugin-api/beta/client/services'; -export const showSignInDialog = () => (dispatch, getState) => { - if (isSlotEmpty('login')) { - return; - } - const signInPopUp = window.open( - '/embed/stream/login', - 'Login', - 'menubar=0,resizable=0,width=500,height=550,top=200,left=500' - ); +export const showSignInDialog = () => ({ + type: actions.SHOW_SIGNIN_DIALOG, +}); - // Workaround odd behavior in older WebKit versions, where - // onunload is called twice. (Encountered in IOS 8.3) - let loaded = false; - signInPopUp.onload = () => { - loaded = true; - - // Fire some actions inside the popups reducer, to initialize required state. - const required = getState().asset.toJS().settings.requireEmailConfirmation; - const redirectUri = getState().auth.toJS().redirectUri; - signInPopUp.coralStore.dispatch(setRequireEmailVerification(required)); - signInPopUp.coralStore.dispatch(setRedirectUri(redirectUri)); - }; - - // Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari. - signInPopUp.onunload = () => { - if (loaded) { - dispatch(checkLogin()); - } - }; - - dispatch({type: actions.SHOW_SIGNIN_DIALOG}); -}; export const hideSignInDialog = () => (dispatch) => { + if (window.opener && window.opener !== window) { + + // TODO: We need to address this when we refactor the + // login popup out of the embed. + + // we are in a popup + window.close(); + } else { + dispatch(checkLogin()); + } dispatch({type: actions.HIDE_SIGNIN_DIALOG}); - window.close(); }; +export const focusSignInDialog = () => ({ + type: actions.FOCUS_SIGNIN_DIALOG, +}); + +export const blurSignInDialog = () => ({ + type: actions.BLUR_SIGNIN_DIALOG, +}); + export const createUsernameRequest = () => ({ type: actions.CREATE_USERNAME_REQUEST }); diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js new file mode 100644 index 000000000..b4dacf1eb --- /dev/null +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {isSlotEmpty} from 'coral-framework/helpers/plugins'; +import PropTypes from 'prop-types'; + +function IfSlotIsEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) { + return ( + + {isSlotEmpty(slot, pluginConfig, rest) ? children : null} + + ); +} + +IfSlotIsEmpty.propTypes = { + slot: PropTypes.string, + className: PropTypes.string, +}; + +const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); + +export default connect(mapStateToProps, null)(IfSlotIsEmpty); + diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js new file mode 100644 index 000000000..820a528fe --- /dev/null +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {isSlotEmpty} from 'coral-framework/helpers/plugins'; +import PropTypes from 'prop-types'; + +function IfSlotIsNotEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) { + return ( + + {!isSlotEmpty(slot, pluginConfig, rest) ? children : null} + + ); +} + +IfSlotIsNotEmpty.propTypes = { + slot: PropTypes.string, + className: PropTypes.string, +}; + +const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); + +export default connect(mapStateToProps, null)(IfSlotIsNotEmpty); + diff --git a/client/coral-framework/components/Popup.js b/client/coral-framework/components/Popup.js new file mode 100644 index 000000000..b9393a36d --- /dev/null +++ b/client/coral-framework/components/Popup.js @@ -0,0 +1,152 @@ +import {Component} from 'react'; +import PropTypes from 'prop-types'; + +export default class Popup extends Component { + ref = null; + detectCloseInterval = null; + + constructor(props) { + super(props); + + if (props.open) { + this.openWindow(props); + } + } + + openWindow(props = this.props) { + this.ref = window.open( + props.href, + props.title, + props.features, + ); + + this.setCallbacks(); + } + + setCallbacks() { + this.ref.onload = () => { + clearInterval(this.detectCloseInterval); + this.onLoad(); + }; + + this.ref.onfocus = () => { + this.onFocus(); + }; + + this.ref.onblur = () => { + this.onBlur(); + }; + + // Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari. + this.ref.onunload = () => { + this.onUnload(); + + const interval = setInterval(() => { + if (this.ref.onload === null) { + this.setCallbacks(); + clearInterval(interval); + } + }, 50); + + this.detectCloseInterval = setInterval(() => { + if (this.ref.closed) { + clearInterval(this.detectCloseInterval); + this.onClose(); + } + }, 50); + }; + } + + closeWindow() { + if (this.ref) { + if (!this.ref.closed) { + this.ref.close(); + } + this.ref = null; + } + } + + focusWindow() { + if (this.ref && !this.ref.closed) { + this.ref.focus(); + } + } + + blurWindow() { + if (this.ref && !this.ref.closed) { + this.ref.blur(); + } + } + + onLoad = () => { + if (this.props.onLoad) { + this.props.onLoad(); + } + } + + onUnload = () => { + if (this.props.onUnload) { + this.props.onUnload(); + } + } + + onClose = () => { + if (this.props.onClose) { + this.props.onClose(); + } + } + + onFocus = () => { + if (this.props.onFocus) { + this.props.onFocus(); + } + } + + onBlur = () => { + if (this.props.onBlur) { + this.props.onBlur(); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.open && !this.ref) { + this.openWindow(nextProps); + } + + if (this.props.open && !nextProps.open) { + this.closeWindow(); + } + + if (!this.props.focus && nextProps.focus) { + this.focusWindow(); + } + + if (this.props.focus && !nextProps.focus) { + this.blurWindow(); + } + + if (this.props.href !== nextProps.href) { + this.ref.location.href = nextProps.href; + } + } + + componentWillUnmount() { + this.closeWindow(); + } + + render() { + return null; + } +} + +Popup.propTypes = { + open: PropTypes.bool, + focus: PropTypes.bool, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onLoad: PropTypes.func, + onUnload: PropTypes.func, + onClose: PropTypes.func, + href: PropTypes.string.isRequired, + features: PropTypes.string, +}; diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index ca246591b..2861a451c 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -4,14 +4,14 @@ import styles from './Slot.css'; import {connect} from 'react-redux'; import {getSlotElements} from 'coral-framework/helpers/plugins'; -function Slot ({fill, inline = false, className, plugin_config: config, defaultComponent: DefaultComponent, ...rest}) { - let children = getSlotElements(fill, {...rest, config}); +function Slot ({fill, inline = false, className, pluginConfig = {}, defaultComponent: DefaultComponent, ...rest}) { + let children = getSlotElements(fill, pluginConfig, rest); if (children.length === 0 && DefaultComponent) { children = ; } return ( -
+
{children}
); @@ -21,7 +21,7 @@ Slot.propTypes = { fill: React.PropTypes.string }; -const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config}); +const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); export default connect(mapStateToProps, null)(Slot); diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 57bde6f3f..324f75f44 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -3,6 +3,8 @@ export const CLEAN_STATE = 'CLEAN_STATE'; export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; +export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG'; +export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG'; export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST'; export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS'; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 625a0ee3f..55fdaf6c5 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -7,32 +7,31 @@ import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; import {loadTranslations} from 'coral-framework/services/i18n'; -import {injectReducers, getStore} from 'coral-framework/services/store'; +import {injectReducers} from 'coral-framework/services/store'; import camelize from './camelize'; -export function getSlotComponents(slot) { - const pluginConfig = getStore().getState().config.plugin_config; - +export function getSlotComponents(slot, pluginConfig, props = {}) { return flatten(plugins - // Filter out components that have slots and have been disabled in `plugin_config` + // Filter out components that have slots and have been disabled in `plugin_config` .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) .filter((o) => o.module.slots[slot]) .map((o) => o.module.slots[slot]) - ); + ) + .filter((component) => !component.isExcluded || !component.isExcluded({...props, config: pluginConfig})); } -export function isSlotEmpty(slot) { - return getSlotComponents(slot).length === 0; +export function isSlotEmpty(slot, pluginConfig, props) { + return getSlotComponents(slot, pluginConfig, props).length === 0; } /** * Returns React Elements for given slot. */ -export function getSlotElements(slot, props = {}) { - return getSlotComponents(slot) - .map((component, i) => React.createElement(component, {key: i, ...props})); +export function getSlotElements(slot, pluginConfig, props = {}) { + return getSlotComponents(slot, pluginConfig, props) + .map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig})); } function getComponentFragments(components) { diff --git a/client/coral-framework/hocs/excludeIf.js b/client/coral-framework/hocs/excludeIf.js new file mode 100644 index 000000000..ec2f0f27a --- /dev/null +++ b/client/coral-framework/hocs/excludeIf.js @@ -0,0 +1,4 @@ +export default (condition) => (BaseComponent) => { + BaseComponent.isExcluded = condition; + return BaseComponent; +}; diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index d62d62ae3..62e96444c 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,14 +1,5 @@ -import React from 'react'; -import {getDisplayName} from '../helpers/hoc'; - // TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38. -export default (fragments) => (WrappedComponent) => { - class WithFragments extends React.Component { - render() { - return ; - } - } - WithFragments.fragments = fragments; - WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`; - return WithFragments; +export default (fragments) => (BaseComponent) => { + BaseComponent.fragments = fragments; + return BaseComponent; }; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 92d676abe..5481b7a5e 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -7,6 +7,7 @@ const initialState = Map({ loggedIn: false, user: null, showSignInDialog: false, + signInDialogFocus: false, showCreateUsernameDialog: false, checkedInitialLogin: false, view: 'SIGNIN', @@ -29,13 +30,21 @@ const purge = (user) => { export default function auth (state = initialState, action) { switch (action.type) { + case actions.FOCUS_SIGNIN_DIALOG: + return state + .set('signInDialogFocus', true); + case actions.BLUR_SIGNIN_DIALOG: + return state + .set('signInDialogFocus', false); case actions.SHOW_SIGNIN_DIALOG : return state - .set('showSignInDialog', true); + .set('showSignInDialog', true) + .set('signInDialogFocus', true); case actions.HIDE_SIGNIN_DIALOG : return state.merge(Map({ isLoading: false, showSignInDialog: false, + signInDialogFocus: false, view: 'SIGNIN', error: '', passwordRequestFailure: null, diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 7f3476cfd..ed8b5cec4 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -1,2 +1,3 @@ export {default as withReaction} from './withReaction'; export {default as withFragments} from 'coral-framework/hocs/withFragments'; +export {default as excludeIf} from 'coral-framework/hocs/excludeIf'; diff --git a/plugin-api/beta/client/selectors/index.js b/plugin-api/beta/client/selectors/index.js new file mode 100644 index 000000000..09e25d453 --- /dev/null +++ b/plugin-api/beta/client/selectors/index.js @@ -0,0 +1 @@ +export const pluginConfigSelector = (state) => state.config.pluginConfig; diff --git a/plugins/talk-plugin-featured/client/containers/Tab.js b/plugins/talk-plugin-featured/client/containers/Tab.js index 0bcb49e13..f5d5678e6 100644 --- a/plugins/talk-plugin-featured/client/containers/Tab.js +++ b/plugins/talk-plugin-featured/client/containers/Tab.js @@ -1,5 +1,6 @@ import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; +import excludeIf from 'coral-framework/hocs/excludeIf'; import Tab from '../components/Tab'; // TODO: This is just example code, and needs to replaced by an actual implementation. @@ -12,6 +13,7 @@ const enhance = compose( } }`, }), + excludeIf((props) => props.asset.recentComments.length === 0) ); export default enhance(Tab);