From f9d4597d63d7bd2c558cdb5cde5c0e4b4ae13caf Mon Sep 17 00:00:00 2001 From: undemian Date: Tue, 3 Apr 2018 17:38:35 +0300 Subject: [PATCH 1/3] Add layout option --- docs/API.md | 1 + src/api.js | 16 +++++++++----- src/index.js | 23 +++++++++++++++----- src/ui/components/happychat-form/style.scss | 13 ++++++++++- src/ui/css/_main.scss | 4 ++++ targets/wordpress/class-happychat-client.php | 2 +- 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/docs/API.md b/docs/API.md index 1f5f25bd..57f754e2 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 | `panel` | The chat layout fullscreen (100%), panel (floating) | | `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..8c93e82a 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_PANEL } 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 {Array} layout Optional. Happychat layout (panel or fullscreen) default value is panel * @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_PANEL, 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/index.js b/src/index.js index b4bc4f37..eee7a127 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,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 +27,7 @@ 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_FULLSCREEN } from 'src/constants'; const store = createStore( reducer, @@ -47,7 +48,7 @@ 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' ); const primaryHasAnySecondary = options => @@ -70,6 +71,11 @@ const createIframe = ( props, assetsLoadedHook = () => {} ) => { iframeElement.frameBorder = 0; iframeElement.scrolling = 'no'; + // full height for fullescreen layout + if ( layout === LAYOUT_FULLSCREEN ) { + iframeElement.height = '100%'; + } + document.getElementById( nodeId ).appendChild( iframeElement ); // Force FF (and maybe other browsers?) to write the changes to this iframe; @@ -156,6 +162,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 ( layout === LAYOUT_FULLSCREEN ) { + 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 +200,7 @@ export const renderHappychat = ( username, display_name, avatar_URL, - language, + localeSlug, }, groups = [ HAPPYCHAT_GROUP_WPCOM ], canChat = true, @@ -208,7 +219,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 +235,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/style.scss b/src/ui/components/happychat-form/style.scss index fb0c1dae..ff718426 100644 --- a/src/ui/components/happychat-form/style.scss +++ b/src/ui/components/happychat-form/style.scss @@ -2,6 +2,13 @@ // experiment: style scrollbars .happychat__page { + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + align-content: stretch; + ::-webkit-scrollbar { width: 12px; height: 12px; @@ -27,6 +34,11 @@ border: 1px solid var(--neutral-lighten-25); } +// override for fullscreen +body.is-fullscreen .happychat__page { + border: 0; +} + // make textarea to fill the whole box .happychat__message > textarea { width: 100%; @@ -35,7 +47,6 @@ // small tweaks to conversation container .happychat__conversation { - height: 80vh; // set height so it can be scrolled padding-left: 6px; padding-right: 6px; } diff --git a/src/ui/css/_main.scss b/src/ui/css/_main.scss index 1db465b0..8bcddfc8 100644 --- a/src/ui/css/_main.scss +++ b/src/ui/css/_main.scss @@ -13,6 +13,10 @@ body { margin: 1; // separate from iframe, so children can style the borders } +body.is-fullscreen { + margin: 0; +} + a, a:visited { color: var(--focused); diff --git a/targets/wordpress/class-happychat-client.php b/targets/wordpress/class-happychat-client.php index 033e40e4..7dac8ca8 100644 --- a/targets/wordpress/class-happychat-client.php +++ b/targets/wordpress/class-happychat-client.php @@ -87,7 +87,7 @@ private function validate_path( $path ) { private function get_happychat_settings() { $happychat_settings = [ 'accessToken' => null, - 'entry' => 'ENTRY_FORM', + 'entry' => 'form', 'entryOptions' => [], 'groups' => [ get_option( 'happychat_user_group' ) ], 'canChat' => 'true', From 4509d2de25d91058ac889a13bb8b5182216b7946 Mon Sep 17 00:00:00 2001 From: undemian Date: Wed, 11 Apr 2018 00:29:32 +0300 Subject: [PATCH 2/3] Add latest layouts --- src/api.js | 4 +-- src/constants.js | 6 ++-- src/form.js | 3 ++ src/form.scss | 1 + src/index.js | 42 +++++++++++++++++------ src/ui/components/happychat-form/index.js | 8 +++++ src/ui/components/title/index.jsx | 8 ++--- src/ui/components/title/style.scss | 18 +++++++--- 8 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/api.js b/src/api.js index 8c93e82a..89677b49 100644 --- a/src/api.js +++ b/src/api.js @@ -15,7 +15,7 @@ import { renderError, } from './index'; import authenticator from 'src/lib/auth'; -import { LAYOUT_PANEL } from './constants'; +import { LAYOUT_MAX_WIDTH_FIXED_HEIGHT } from './constants'; const api = { /** @@ -42,7 +42,7 @@ const api = { entry, entryOptions, groups, - layout = LAYOUT_PANEL, + layout = LAYOUT_MAX_WIDTH_FIXED_HEIGHT, nodeId, user, } ) => { 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 eee7a127..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 @@ -27,7 +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_FULLSCREEN } from 'src/constants'; +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, @@ -51,6 +58,10 @@ const createIframe = ( props, assetsLoadedHook = () => {} ) => { 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 ); @@ -60,22 +71,33 @@ 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'; - // full height for fullescreen layout - if ( layout === LAYOUT_FULLSCREEN ) { - iframeElement.height = '100%'; - } - document.getElementById( nodeId ).appendChild( iframeElement ); // Force FF (and maybe other browsers?) to write the changes to this iframe; @@ -163,7 +185,7 @@ const createIframe = ( props, assetsLoadedHook = () => {} ) => { iframeElement.contentDocument.body.classList.add( hasTouch() ? 'touch' : 'notouch' ); // add class for fullscreen - if ( layout === LAYOUT_FULLSCREEN ) { + if ( includes( [ LAYOUT_MAX_PARENT_SIZE, LAYOUT_PANEL_MAX_PARENT_SIZE ], layout ) ) { iframeElement.contentDocument.body.classList.add( 'is-fullscreen' ); } 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 (
+ { includes( [ LAYOUT_PANEL_FIXED_SIZE, LAYOUT_PANEL_MAX_PARENT_SIZE ], layout ) && + } + <Timeline currentUserEmail={ currentUserEmail } currentUserGroup={ currentUserGroup } @@ -97,6 +104,7 @@ HappychatForm.propTypes = { isCurrentUser: PropTypes.func, isExternalUrl: PropTypes.func, isServerReachable: PropTypes.bool, + layout: PropTypes.string, message: PropTypes.string, onSendMessage: PropTypes.func, onSendNotTyping: PropTypes.func, diff --git a/src/ui/components/title/index.jsx b/src/ui/components/title/index.jsx index 31e3da12..72ae0880 100644 --- a/src/ui/components/title/index.jsx +++ b/src/ui/components/title/index.jsx @@ -9,10 +9,10 @@ import GridiconCross from 'gridicons/dist/cross'; /* * React component for rendering title bar */ -export const Title = ( { onCloseChat, translate } ) => ( - <div className="happychat__title"> - <div className="happychat__active-toolbar"> - <h4>{ translate( 'Support Chat' ) }</h4> +export const Title = ( { onCloseChat } ) => ( + <div className="title__wrapper"> + <div className="title__active-toolbar"> + <h4>{ 'Support Chat' }</h4> <div onClick={ onCloseChat }> <GridiconCross /> </div> diff --git a/src/ui/components/title/style.scss b/src/ui/components/title/style.scss index f6b92ef4..7e5c103c 100644 --- a/src/ui/components/title/style.scss +++ b/src/ui/components/title/style.scss @@ -1,10 +1,18 @@ /** @format */ +.title__wrapper { + cursor: default; + padding: 0; + background: $blue-medium; + border-bottom: 1px solid darken( $blue-medium, 5% ); + color: $white; + flex: 0 0 auto; + display: flex; + align-items: center; + line-height: 32px; + line-height: 46px; +} -/** - * Live chat - */ - -.happychat__active-toolbar { +.title__active-toolbar { display: flex; flex: 1 1 auto; From 01dce4358ab5c33f34df15f5a8f70d9d7f7540b9 Mon Sep 17 00:00:00 2001 From: undemian <undemian@gmail.com> Date: Wed, 11 Apr 2018 00:32:55 +0300 Subject: [PATCH 3/3] Add docs --- docs/API.md | 2 +- src/api.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index 57f754e2..a48cb8ea 100644 --- a/docs/API.md +++ b/docs/API.md @@ -19,7 +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 | `panel` | The chat layout fullscreen (100%), panel (floating) | +| `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 89677b49..7870ebfa 100644 --- a/src/api.js +++ b/src/api.js @@ -32,7 +32,7 @@ const api = { * 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 {Array} layout Optional. Happychat layout (panel or fullscreen) default value is panel + * @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 . */