diff --git a/docs/API.md b/docs/API.md index 1f5f25bd..a48cb8ea 100644 --- a/docs/API.md +++ b/docs/API.md @@ -19,6 +19,7 @@ with the following top-level properties: | `entryOptions` | object | Optional | `{}` | Configures happychat entry points. See details below. | | `groups` | array | Optional | `[WP.com]` | What group the chat session should be routed to. Valid values are `WP.com`, `woo`, and `jpop`. | | `canChat` | bool | Optional | `true` | Whether the user can be offered chat or not. | +| `layout` | string | Optional | `max-width-fixed-height` | The chat layout `max-width-fixed-height`\|`max-parent-size`\|`panel-fixed-size`\|`panel-max-parent-size` | | `nodeId` | string | Mandatory | `null` | The id of the HTMLNode where Happychat will be rendered. | ### The entry prop diff --git a/src/api.js b/src/api.js index c5f9d0d1..7870ebfa 100644 --- a/src/api.js +++ b/src/api.js @@ -15,6 +15,7 @@ import { renderError, } from './index'; import authenticator from 'src/lib/auth'; +import { LAYOUT_MAX_WIDTH_FIXED_HEIGHT } from './constants'; const api = { /** @@ -26,13 +27,14 @@ const api = { * @param {Object} authentication.options.token Optional. WP.com oAuth access token to be used * @param {Object} authentication.options.proxy Optional. WP.com proxy object to be used * @param {boolean} canChat Optional. Whether the user can be offered chat. True by default. - * @param {string} entry Optional. Valid values are ENTRY_FORM, ENTRY_CHAT. + * @param {string} entry Optional. Valid values are 'form', 'chat'. + * ENTRY_FORM (constant for 'form') is the default and will render the contact form. + * ENTRY_CHAT (constant for 'chat') will render the chat form. * @param {Object} entryOptions Optional. Contains options to configure the selected entry. * @param {Array} groups Mandatory. Happychat groups this user belongs to. + * @param {String} layout Optional. The chat layout max-width-fixed-height | max-parent-size | panel-fixed-size | panel-max-parent-size * @param {string} nodeId Mandatory. HTML Node id where Happychat will be rendered. * @param {Object} user Optional. Customer information . - * ENTRY_FORM is the default and will render the contact form. - * ENTRY_CHAT will render the chat form. */ open: ( { authentication, @@ -40,22 +42,24 @@ const api = { entry, entryOptions, groups, + layout = LAYOUT_MAX_WIDTH_FIXED_HEIGHT, nodeId, user, } ) => { authenticator.init( authentication ); - const targetNode = createTargetNode( { nodeId, groups, entryOptions } ); + const targetNode = createTargetNode( { entryOptions, groups, layout, nodeId } ); authenticator.login() .then( () => isEmpty( user ) ? authenticator.getUser() : Promise.resolve( user ) ) .then( userObject => renderHappychat( targetNode, { - userObject, canChat, - groups, entry, entryOptions, + groups, + layout, + userObject, } ) ) .catch( error => renderError( targetNode, { error } ) ); diff --git a/src/constants.js b/src/constants.js index f91638ff..fe82eea1 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,8 +3,10 @@ export const AUTH_TYPE_WPCOM_OAUTH_BY_TOKEN = 'wpcom-oauth-by-token'; export const AUTH_TYPE_WPCOM_PROXY_IFRAME = 'wpcom-proxy-iframe'; // Layouts -export const LAYOUT_FULLSCREEN = 'fullscreen'; -export const LAYOUT_PANEL = 'panel'; +export const LAYOUT_MAX_WIDTH_FIXED_HEIGHT = 'max-width-fixed-height'; +export const LAYOUT_MAX_PARENT_SIZE = 'max-parent-size'; +export const LAYOUT_PANEL_FIXED_SIZE = 'panel-fixed-size'; +export const LAYOUT_PANEL_MAX_PARENT_SIZE = 'panel-max-parent-size'; // Entry export const ENTRY_CHAT = 'chat'; diff --git a/src/form.js b/src/form.js index 72b97808..0f929ac1 100644 --- a/src/form.js +++ b/src/form.js @@ -78,6 +78,7 @@ class ChatComponent { isCurrentUser, isExternalUrl, isServerReachable, + layout, message, onSendMessage, onSendNotTyping, @@ -100,6 +101,7 @@ class ChatComponent { isCurrentUser={ isCurrentUser } isExternalUrl={ isExternalUrl } isServerReachable={ isServerReachable } + layout={ layout } message={ message } onSendMessage={ onSendMessage } onSendNotTyping={ onSendNotTyping } @@ -366,6 +368,7 @@ Form.propTypes = { canChat: PropTypes.bool, entry: PropTypes.string, entryOptions: PropTypes.object, + layout: PropTypes.string, }; // Whether URL should open a new tab or not. diff --git a/src/form.scss b/src/form.scss index 6c5620d6..82dfe033 100644 --- a/src/form.scss +++ b/src/form.scss @@ -40,6 +40,7 @@ @import 'ui/components/timeline/style'; @import 'ui/components/notices/style'; @import 'ui/components/composer/style'; +@import 'ui/components/title/style'; // main components @import 'ui/components/happychat-form/style'; diff --git a/src/index.js b/src/index.js index b4bc4f37..b2b5ab2a 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import { Provider } from 'react-redux'; import { applyMiddleware, createStore, compose } from 'redux'; import { devToolsEnhancer } from 'redux-devtools-extension'; import find from 'lodash/find'; +import includes from 'lodash/includes'; /** * Internal dependencies @@ -16,7 +17,7 @@ import find from 'lodash/find'; // utils import { hasTouch } from 'src/lib/touch-detect'; // UI components -import Happychat, { ENTRY_FORM } from 'src/form'; +import Happychat from 'src/form'; import { MessageForm } from 'src/ui/components/message-form'; // state: general, actions, selectors import eventAPIFactory from 'src/state/event-api'; @@ -27,6 +28,13 @@ import { setAssetsLoaded } from 'src/state/ui/actions'; import { setCurrentUser, setGroups, setLocale, setEligibility } from 'src/state/user/actions'; import { setFallbackTicketOptions } from 'src/state/fallbackTicket/actions'; import config from 'src/config'; +import { + ENTRY_FORM, + LAYOUT_MAX_WIDTH_FIXED_HEIGHT, + LAYOUT_MAX_PARENT_SIZE, + LAYOUT_PANEL_FIXED_SIZE, + LAYOUT_PANEL_MAX_PARENT_SIZE, +} from 'src/constants'; const store = createStore( reducer, @@ -47,9 +55,13 @@ const dispatchAssetsFinishedDownloading = () => store.dispatch( setAssetsLoaded( * @returns {HTMLNode} Target node where Happychat can hook into. */ const createIframe = ( props, assetsLoadedHook = () => {} ) => { - const { nodeId, groups, entryOptions } = props; + const { entryOptions, groups, layout, nodeId } = props; const iframeElement = document.createElement( 'iframe' ); + let iframeHeight = 0; + let iframeWidth = 0; + switch ( layout ) { + case LAYOUT_MAX_WIDTH_FIXED_HEIGHT: const primaryHasAnySecondary = options => Array.isArray( options ) && find( options, opt => opt.secondaryOptions ); @@ -59,14 +71,30 @@ const createIframe = ( props, assetsLoadedHook = () => {} ) => { // Calculate height based on the number of components // the iframe may need to render. - let iframeHeight = 480; + iframeHeight = 480; iframeHeight = iframeHeight + ( entryOptions && entryOptions.primaryOptions ? 110 : 0 ); iframeHeight = iframeHeight + ( isThereAnySecondaryOptions( entryOptions ) ? 110 : 0 ); iframeHeight = iframeHeight + ( entryOptions && entryOptions.itemList ? 70 : 0 ); + iframeHeight = iframeHeight + 'em'; + iframeWidth = '100%'; + break; + + case LAYOUT_PANEL_FIXED_SIZE: + iframeHeight = '330em'; + iframeWidth = '150em'; + break; + + case LAYOUT_MAX_PARENT_SIZE: + case LAYOUT_PANEL_MAX_PARENT_SIZE: + iframeHeight = '100%'; + iframeWidth = '100%'; + break; + } + // style iframe element - iframeElement.width = '100%'; - iframeElement.height = iframeHeight + 'em'; + iframeElement.width = iframeWidth; + iframeElement.height = iframeHeight; iframeElement.frameBorder = 0; iframeElement.scrolling = 'no'; @@ -156,6 +184,11 @@ const createIframe = ( props, assetsLoadedHook = () => {} ) => { // some CSS styles depend on these top-level classes being present iframeElement.contentDocument.body.classList.add( hasTouch() ? 'touch' : 'notouch' ); + // add class for fullscreen + if ( includes( [ LAYOUT_MAX_PARENT_SIZE, LAYOUT_PANEL_MAX_PARENT_SIZE ], layout ) ) { + iframeElement.contentDocument.body.classList.add( 'is-fullscreen' ); + } + // React advises to use an element -not the body itself- as the target render, // that's why we create this wrapperElement inside the iframe. const targetNode = document.createElement( 'div' ); @@ -189,7 +222,7 @@ export const renderHappychat = ( username, display_name, avatar_URL, - language, + localeSlug, }, groups = [ HAPPYCHAT_GROUP_WPCOM ], canChat = true, @@ -208,7 +241,7 @@ export const renderHappychat = ( } ) ); store.dispatch( setGroups( groups ) ); - store.dispatch( setLocale( language ) ); + store.dispatch( setLocale( localeSlug ) ); store.dispatch( setFallbackTicketOptions( fallbackTicket ) ); isAnyCanChatPropFalse( canChat, entryOptions ) @@ -224,8 +257,8 @@ export const renderHappychat = ( }; /* eslint-enable camelcase */ -export const createTargetNode = ( { nodeId, groups, entryOptions } ) => { - return createIframe( { nodeId, groups, entryOptions }, dispatchAssetsFinishedDownloading ); +export const createTargetNode = ( props ) => { + return createIframe( props, dispatchAssetsFinishedDownloading ); }; export const renderError = ( targetNode, { error } ) => diff --git a/src/ui/components/happychat-form/index.js b/src/ui/components/happychat-form/index.js index ec293d29..6d3c1df3 100644 --- a/src/ui/components/happychat-form/index.js +++ b/src/ui/components/happychat-form/index.js @@ -5,6 +5,7 @@ */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import includes from 'lodash/includes'; /** * Internal dependencies @@ -12,6 +13,8 @@ import PropTypes from 'prop-types'; import { Composer } from 'src/ui/components/composer'; import { Notices } from 'src/ui/components/notices'; import { Timeline } from 'src/ui/components/timeline'; +import { Title } from 'src/ui/components/title'; +import { LAYOUT_PANEL_FIXED_SIZE, LAYOUT_PANEL_MAX_PARENT_SIZE } from 'src/constants'; /** * React component for rendering a happychat client @@ -47,6 +50,7 @@ export class HappychatForm extends Component { isCurrentUser, isExternalUrl, isServerReachable, + layout, message, onSendMessage, onSendNotTyping, @@ -59,6 +63,9 @@ export class HappychatForm extends Component { return (