Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 10 additions & 6 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
renderError,
} from './index';
import authenticator from 'src/lib/auth';
import { LAYOUT_MAX_WIDTH_FIXED_HEIGHT } from './constants';

const api = {
/**
Expand All @@ -26,36 +27,39 @@ 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,
canChat,
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 } ) );
Expand Down
6 changes: 4 additions & 2 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions src/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class ChatComponent {
isCurrentUser,
isExternalUrl,
isServerReachable,
layout,
message,
onSendMessage,
onSendNotTyping,
Expand All @@ -100,6 +101,7 @@ class ChatComponent {
isCurrentUser={ isCurrentUser }
isExternalUrl={ isExternalUrl }
isServerReachable={ isServerReachable }
layout={ layout }
message={ message }
onSendMessage={ onSendMessage }
onSendNotTyping={ onSendNotTyping }
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
51 changes: 42 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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
*/
// 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';
Expand All @@ -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,
Expand All @@ -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 );

Expand All @@ -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';

Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -189,7 +222,7 @@ export const renderHappychat = (
username,
display_name,
avatar_URL,
language,
localeSlug,
},
groups = [ HAPPYCHAT_GROUP_WPCOM ],
canChat = true,
Expand All @@ -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 )
Expand All @@ -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 } ) =>
Expand Down
8 changes: 8 additions & 0 deletions src/ui/components/happychat-form/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import includes from 'lodash/includes';

/**
* Internal dependencies
*/
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
Expand Down Expand Up @@ -47,6 +50,7 @@ export class HappychatForm extends Component {
isCurrentUser,
isExternalUrl,
isServerReachable,
layout,
message,
onSendMessage,
onSendNotTyping,
Expand All @@ -59,6 +63,9 @@ export class HappychatForm extends Component {

return (
<div className="happychat__page" aria-live="polite" aria-relevant="additions">
{ includes( [ LAYOUT_PANEL_FIXED_SIZE, LAYOUT_PANEL_MAX_PARENT_SIZE ], layout ) &&
<Title /> }

<Timeline
currentUserEmail={ currentUserEmail }
currentUserGroup={ currentUserGroup }
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion src/ui/components/happychat-form/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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%;
Expand All @@ -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;
}
Expand Down
8 changes: 4 additions & 4 deletions src/ui/components/title/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
18 changes: 13 additions & 5 deletions src/ui/components/title/style.scss
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
4 changes: 4 additions & 0 deletions src/ui/css/_main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion targets/wordpress/class-happychat-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down