diff --git a/packages/botonic-react/src/experimental/constants.js b/packages/botonic-react/src/experimental/constants.js
new file mode 100644
index 0000000000..4b14fbade1
--- /dev/null
+++ b/packages/botonic-react/src/experimental/constants.js
@@ -0,0 +1,189 @@
+import BotonicLogo from '../assets/botonic_react_logo100x100.png'
+
+export const SENDERS = {
+ bot: 'bot',
+ user: 'user',
+}
+
+export const COLORS = {
+ // http://chir.ag/projects/name-that-color
+ APPLE_GREEN: 'rgba(58, 156, 53, 1)',
+ BLEACHED_CEDAR_PURPLE: 'rgba(46, 32, 59, 1)',
+ BOTONIC_BLUE: 'rgba(0, 153, 255, 1)',
+ CACTUS_GREEN: 'rgba(96, 115, 94, 1)',
+ CONCRETE_WHITE: 'rgba(243, 243, 243, 1)',
+ CURIOUS_BLUE: 'rgba(38, 139, 210, 1)',
+ DAINTREE_BLUE: 'rgba(0, 43, 53, 1)',
+ ERROR_RED: 'rgba(255, 43, 94)',
+ FRINGY_FLOWER_GREEN: 'rgba(198, 231, 192, 1)',
+ GRAY: 'rgba(129, 129, 129, 1)',
+ LIGHT_GRAY: 'rgba(209, 209, 209, 1)',
+ MID_GRAY: 'rgba(105, 105, 115, 1)',
+ PIGEON_POST_BLUE_ALPHA_0_5: 'rgba(176, 196, 222, 0.5)',
+ SCORPION_GRAY: 'rgba(87, 87, 87, 1)',
+ SEASHELL_WHITE: 'rgba(241, 240, 240, 1)',
+ SILVER: 'rgba(200, 200, 200, 1)',
+ SOLID_BLACK_ALPHA_0_2: 'rgba(0, 0, 0, 0.2)',
+ SOLID_BLACK_ALPHA_0_5: 'rgba(0, 0, 0, 0.5)',
+ SOLID_BLACK: 'rgba(0, 0, 0, 1)',
+ SOLID_WHITE_ALPHA_0_2: 'rgba(255, 255, 255, 0.2)',
+ SOLID_WHITE_ALPHA_0_8: 'rgba(255, 255, 255, 0.8)',
+ SOLID_WHITE: 'rgba(255, 255, 255, 1)',
+ TASMAN_GRAY: 'rgba(209, 216, 207, 1)',
+ TRANSPARENT: 'rgba(0, 0, 0, 0)',
+ WILD_SAND_WHITE: 'rgba(244, 244, 244, 1)',
+}
+
+export const WEBCHAT = {
+ DEFAULTS: {
+ WIDTH: 300,
+ HEIGHT: 450,
+ TITLE: 'Botonic',
+ LOGO: BotonicLogo,
+ PLACEHOLDER: 'Ask me something...',
+ FONT_FAMILY: "'Noto Sans JP', sans-serif",
+ BORDER_RADIUS_TOP_CORNERS: '6px 6px 0px 0px',
+ ELEMENT_WIDTH: 222,
+ ELEMENT_MARGIN_RIGHT: 6,
+ STORAGE_KEY: 'botonicState',
+ HOST_ID: 'root',
+ ID: 'botonic-webchat',
+ BUTTON_AUTO_DISABLE: false,
+ BUTTON_DISABLED_STYLE: {
+ opacity: 0.5,
+ cursor: 'auto',
+ pointerEvents: 'none',
+ },
+ },
+ SELECTORS: {
+ SCROLLABLE_CONTENT: '#botonic-scrollable-content',
+ SIMPLEBAR_CONTENT: '.simplebar-content',
+ SIMPLEBAR_WRAPPER: '.simplebar-content-wrapper',
+ },
+ CUSTOM_PROPERTIES: {
+ // General
+ enableAnimations: 'animations.enable',
+ markdownStyle: 'markdownStyle',
+ scrollbar: 'scrollbar',
+ // Mobile
+ mobileBreakpoint: 'mobileBreakpoint',
+ mobileStyle: 'mobileStyle',
+ // Webviews
+ webviewHeaderStyle: 'webview.header.style',
+ webviewStyle: 'webview.style',
+ // Brand
+ brandColor: 'brand.color',
+ brandImage: 'brand.image',
+ // Header
+ customHeader: 'header.custom',
+ headerImage: 'header.image',
+ headerStyle: 'header.style',
+ headerSubtitle: 'header.subtitle',
+ headerTitle: 'header.title',
+ // Bot Message
+ botMessageBackground: 'message.bot.style.background',
+ botMessageBlobTick: 'message.bot.blobTick',
+ botMessageBlobTickStyle: 'message.bot.blobTickStyle',
+ botMessageBlobWidth: 'message.bot.blobWidth',
+ botMessageBorderColor: 'message.bot.style.borderColor',
+ botMessageImage: 'message.bot.image',
+ botMessageImageStyle: 'message.bot.imageStyle',
+ botMessageStyle: 'message.bot.style',
+ // User Message
+ customMessageTypes: 'message.customTypes',
+ messageStyle: 'message.style',
+ userMessageBackground: 'message.user.style.background',
+ userMessageBlobTick: 'message.user.blobTick',
+ userMessageBlobTickStyle: 'message.user.blobTickStyle',
+ userMessageBorderColor: 'message.user.style.borderColor',
+ userMessageStyle: 'message.user.style',
+ // Timestamps
+ enableMessageTimestamps: 'message.timestamps.enable',
+ messageTimestampsFormat: 'message.timestamps.format',
+ messageTimestampsStyle: 'message.timestamps.style',
+ // Intro
+ customIntro: 'intro.custom',
+ introImage: 'intro.image',
+ introStyle: 'intro.style',
+ // Buttons
+ buttonHoverBackground: 'button.hoverBackground',
+ buttonHoverTextColor: 'button.hoverTextColor',
+ buttonMessageType: 'button.messageType',
+ buttonStyle: 'button.style',
+ buttonDisabledStyle: 'button.disabledstyle',
+ buttonAutoDisable: 'button.autodisable',
+ buttonStyleBackground: 'button.style.background',
+ buttonStyleColor: 'button.style.color',
+ customButton: 'button.custom',
+ // Replies
+ alignReplies: 'replies.align',
+ customReply: 'reply.custom',
+ replyStyle: 'reply.style',
+ wrapReplies: 'replies.wrap',
+ // TriggerButton
+ customTrigger: 'triggerButton.custom',
+ triggerButtonImage: 'triggerButton.image',
+ triggerButtonStyle: 'triggerButton.style',
+ // User Input
+ blockInputs: 'userInput.blockInputs',
+ documentDownload: 'documentDownload',
+ customMenuButton: 'userInput.menuButton.custom',
+ customPersistentMenu: 'userInput.menu.custom',
+ customSendButton: 'userInput.sendButton.custom',
+ darkBackgroundMenu: 'userInput.menu.darkBackground',
+ enableAttachments: 'userInput.attachments.enable',
+ enableEmojiPicker: 'userInput.emojiPicker.enable',
+ enableSendButton: 'userInput.sendButton.enable',
+ enableUserInput: 'userInput.enable',
+ persistentMenu: 'userInput.persistentMenu',
+ textPlaceholder: 'userInput.box.placeholder',
+ userInputBoxStyle: 'userInput.box.style',
+ userInputStyle: 'userInput.style',
+ // Cover Component
+ coverComponent: 'coverComponent.component',
+ coverComponentProps: 'coverComponent.props',
+ // Carousel
+ customCarouselLeftArrow: 'carousel.arrow.left',
+ customCarouselRightArrow: 'carousel.arrow.right',
+ enableCarouselArrows: 'carousel.enableArrows',
+ },
+}
+
+export const MIME_WHITELIST = {
+ audio: ['audio/mpeg', 'audio/mp3'],
+ document: ['application/pdf'],
+ image: ['image/jpeg', 'image/png'],
+ video: ['video/mp4', 'video/quicktime'],
+}
+
+export const MAX_ALLOWED_SIZE_MB = 10
+
+export const ROLES = {
+ ATTACHMENT_ICON: 'attachment-icon',
+ EMOJI_PICKER_ICON: 'emoji-picker-icon',
+ EMOJI_PICKER: 'emoji-picker',
+ HEADER: 'header',
+ MESSAGE_LIST: 'message-list',
+ PERSISTENT_MENU_ICON: 'persistent-menu-icon',
+ PERSISTENT_MENU: 'persistent-menu',
+ SEND_BUTTON_ICON: 'send-button-icon',
+ WEBCHAT: 'webchat',
+ TRIGGER_BUTTON: 'trigger-button',
+ TYPING_INDICATOR: 'typing-indicator',
+ TEXT_BOX: 'textbox',
+ WEBVIEW: 'webview',
+ WEBVIEW_HEADER: 'webview-header',
+ MESSAGE: 'message',
+ IMAGE_MESSAGE: 'image-message',
+ AUDIO_MESSAGE: 'audio-message',
+ VIDEO_MESSAGE: 'video-message',
+ DOCUMENT_MESSAGE: 'document-message',
+ RAW_MESSAGE: 'raw-message',
+}
+
+export const COMPONENT_TYPE = {
+ TEXT: 'Text',
+ BUTTON: 'Button',
+ REPLY: 'Reply',
+ CAROUSEL: 'Carousel',
+}
diff --git a/packages/botonic-react/src/experimental/contexts.jsx b/packages/botonic-react/src/experimental/contexts.jsx
new file mode 100644
index 0000000000..f4a4f9b8df
--- /dev/null
+++ b/packages/botonic-react/src/experimental/contexts.jsx
@@ -0,0 +1,36 @@
+import React from 'react'
+
+import { webchatInitialState } from './webchat/hooks'
+
+export const RequestContext = React.createContext({
+ getString: () => '',
+ setLocale: () => '',
+ session: {},
+ params: {},
+ input: {},
+ defaultDelay: 0,
+ defaultTyping: 0,
+})
+
+export const WebchatContext = React.createContext({
+ sendText: text => {},
+ sendAttachment: attachment => {},
+ sendPayload: payload => {},
+ sendInput: input => {},
+ setReplies: replies => {},
+ openWebview: webviewComponent => {},
+ addMessage: message => {},
+ updateMessage: message => {},
+ updateReplies: replies => {},
+ updateLatestInput: input => {},
+ closeWebview: () => {},
+ toggleWebchat: () => {},
+ getThemeProperty: property => undefined, // used to retrieve a specific property of the theme defined by the developer in his 'webchat/index.js'
+ resolveCase: () => {},
+ theme: {},
+ webchatState: webchatInitialState,
+ updateWebchatDevSettings: settings => {
+ return {}
+ },
+ updateUser: user => {},
+})
diff --git a/packages/botonic-react/src/experimental/dev-app.jsx b/packages/botonic-react/src/experimental/dev-app.jsx
index 37bfe4da67..5b7ec22341 100644
--- a/packages/botonic-react/src/experimental/dev-app.jsx
+++ b/packages/botonic-react/src/experimental/dev-app.jsx
@@ -2,9 +2,9 @@ import merge from 'lodash.merge'
import React from 'react'
import { render } from 'react-dom'
-import { SENDERS } from '../constants'
-import { onDOMLoaded } from '../util/dom'
+import { SENDERS } from './constants'
import { ReactBot } from './react-bot'
+import { onDOMLoaded } from './util/dom'
import { WebchatDev } from './webchat/webchat-dev'
import { WebchatApp } from './webchat-app'
diff --git a/packages/botonic-react/src/experimental/msg-to-botonic.jsx b/packages/botonic-react/src/experimental/msg-to-botonic.jsx
index b9d974f308..7d86ba958a 100644
--- a/packages/botonic-react/src/experimental/msg-to-botonic.jsx
+++ b/packages/botonic-react/src/experimental/msg-to-botonic.jsx
@@ -1,5 +1,12 @@
import React from 'react'
+import { Button } from '../components/button'
+import { ButtonsDisabler } from '../components/buttons-disabler'
+import { Element } from '../components/element'
+import { Pic } from '../components/pic'
+import { Reply } from '../components/reply'
+import { Subtitle } from '../components/subtitle'
+import { Title } from '../components/title'
import {
isAudio,
isCarousel,
@@ -9,14 +16,7 @@ import {
isLocation,
isText,
isVideo,
-} from '../../src/message-utils'
-import { Button } from '../components/button'
-import { ButtonsDisabler } from '../components/buttons-disabler'
-import { Element } from '../components/element'
-import { Pic } from '../components/pic'
-import { Reply } from '../components/reply'
-import { Subtitle } from '../components/subtitle'
-import { Title } from '../components/title'
+} from '../message-utils'
// Experimental
import { Audio } from './components/audio'
import { Carousel } from './components/carousel'
diff --git a/packages/botonic-react/src/experimental/react-bot.jsx b/packages/botonic-react/src/experimental/react-bot.jsx
index 1ca99f982f..4d0a3b85b4 100644
--- a/packages/botonic-react/src/experimental/react-bot.jsx
+++ b/packages/botonic-react/src/experimental/react-bot.jsx
@@ -1,8 +1,8 @@
import { CoreBot } from '@botonic/core'
import React from 'react'
-import { RequestContext } from '../contexts'
import { Text } from './components/text'
+import { RequestContext } from './contexts'
export class ReactBot extends CoreBot {
constructor(options) {
diff --git a/packages/botonic-react/src/experimental/util/dom.js b/packages/botonic-react/src/experimental/util/dom.js
new file mode 100644
index 0000000000..f7c6b0e8ed
--- /dev/null
+++ b/packages/botonic-react/src/experimental/util/dom.js
@@ -0,0 +1,55 @@
+import { WEBCHAT } from '../constants'
+
+export const getScrollableArea = webchatElement => {
+ const getArea = area => {
+ const botonicScrollableContent = webchatElement.querySelector(
+ WEBCHAT.SELECTORS.SCROLLABLE_CONTENT
+ )
+ const scrollableArea =
+ botonicScrollableContent && botonicScrollableContent.querySelector(area)
+ return scrollableArea
+ }
+ return {
+ full: getArea(WEBCHAT.SELECTORS.SIMPLEBAR_CONTENT),
+ visible: getArea(WEBCHAT.SELECTORS.SIMPLEBAR_WRAPPER),
+ }
+}
+
+export const scrollToBottom = ({
+ timeout = 200,
+ behavior = 'smooth',
+ host,
+} = {}) => {
+ const webchatElement = getWebchatElement(host)
+ if (!webchatElement) return
+ const frame = getScrollableArea(webchatElement).visible
+ if (frame) {
+ setTimeout(
+ () => frame.scrollTo({ top: frame.scrollHeight, behavior: behavior }),
+ timeout
+ )
+ }
+}
+
+export const getWebchatElement = host =>
+ host && host.querySelector(`#${WEBCHAT.DEFAULTS.ID}`)
+
+// https://stackoverflow.com/questions/9457891/how-to-detect-if-domcontentloaded-was-fired
+export const onDOMLoaded = callback => {
+ if (/complete|interactive|loaded/.test(document.readyState)) {
+ // In case the document has finished parsing, document's readyState will
+ // be one of "complete", "interactive" or (non-standard) "loaded".
+ callback()
+ } else {
+ // The document is not ready yet, so wait for the DOMContentLoaded event
+ document.addEventListener('DOMContentLoaded', callback, false)
+ }
+}
+
+export const isShadowDOMSupported = () => {
+ try {
+ return document.head.createShadowRoot || document.head.attachShadow
+ } catch (e) {
+ return false
+ }
+}
diff --git a/packages/botonic-react/src/experimental/webchat-app.jsx b/packages/botonic-react/src/experimental/webchat-app.jsx
index ecdcdcabc3..c8356af188 100644
--- a/packages/botonic-react/src/experimental/webchat-app.jsx
+++ b/packages/botonic-react/src/experimental/webchat-app.jsx
@@ -3,9 +3,9 @@ import merge from 'lodash.merge'
import React, { createRef } from 'react'
import { render } from 'react-dom'
-import { SENDERS, WEBCHAT } from '../constants'
-import { isShadowDOMSupported, onDOMLoaded } from '../util/dom'
+import { SENDERS, WEBCHAT } from './constants'
import { msgToBotonic } from './msg-to-botonic'
+import { isShadowDOMSupported, onDOMLoaded } from './util/dom'
import { Webchat } from './webchat/webchat'
export class WebchatApp {
diff --git a/packages/botonic-react/src/experimental/webchat/actions.jsx b/packages/botonic-react/src/experimental/webchat/actions.jsx
new file mode 100644
index 0000000000..2c11e7acc9
--- /dev/null
+++ b/packages/botonic-react/src/experimental/webchat/actions.jsx
@@ -0,0 +1,22 @@
+export const ADD_MESSAGE = 'addMessage'
+export const ADD_MESSAGE_COMPONENT = 'addMessageComponent'
+export const UPDATE_MESSAGE = 'updateMessage'
+export const UPDATE_REPLIES = 'updateReplies'
+export const UPDATE_LATEST_INPUT = 'updateLatestInput'
+export const UPDATE_TYPING = 'updateTyping'
+export const UPDATE_WEBVIEW = 'updateWebview'
+export const UPDATE_SESSION = 'updateSession'
+export const UPDATE_LAST_ROUTE_PATH = 'updateLastRoutePath'
+export const UPDATE_HANDOFF = 'updateHandoff'
+export const UPDATE_THEME = 'updateTheme'
+export const UPDATE_DEV_SETTINGS = 'updateDevSettings'
+export const TOGGLE_WEBCHAT = 'toggleWebchat'
+export const TOGGLE_EMOJI_PICKER = 'toggleEmojiPicker'
+export const TOGGLE_PERSISTENT_MENU = 'togglePersistentMenu'
+export const TOGGLE_COVER_COMPONENT = 'toggleCoverComponent'
+export const SET_ERROR = 'setError'
+export const CLEAR_MESSAGES = 'clearMessages'
+export const UPDATE_LAST_MESSAGE_DATE = 'updateLastMessageDate'
+export const SET_CURRENT_ATTACHMENT = 'setCurrentAttachment'
+export const SET_ONLINE = 'setOnline'
+export const UPDATE_JWT = 'updateJwt'
diff --git a/packages/botonic-react/src/experimental/webchat/hooks.js b/packages/botonic-react/src/experimental/webchat/hooks.js
new file mode 100644
index 0000000000..ea0d5067d8
--- /dev/null
+++ b/packages/botonic-react/src/experimental/webchat/hooks.js
@@ -0,0 +1,258 @@
+import { useEffect, useMemo, useReducer, useRef, useState } from 'react'
+
+import { COLORS, WEBCHAT } from '../constants'
+import { scrollToBottom } from '../util/dom'
+import {
+ ADD_MESSAGE,
+ ADD_MESSAGE_COMPONENT,
+ CLEAR_MESSAGES,
+ SET_CURRENT_ATTACHMENT,
+ SET_ERROR,
+ SET_ONLINE,
+ TOGGLE_COVER_COMPONENT,
+ TOGGLE_EMOJI_PICKER,
+ TOGGLE_PERSISTENT_MENU,
+ TOGGLE_WEBCHAT,
+ UPDATE_DEV_SETTINGS,
+ UPDATE_HANDOFF,
+ UPDATE_JWT,
+ UPDATE_LAST_MESSAGE_DATE,
+ UPDATE_LAST_ROUTE_PATH,
+ UPDATE_LATEST_INPUT,
+ UPDATE_MESSAGE,
+ UPDATE_REPLIES,
+ UPDATE_SESSION,
+ UPDATE_THEME,
+ UPDATE_TYPING,
+ UPDATE_WEBVIEW,
+} from './actions'
+import { webchatReducer } from './webchat-reducer'
+
+export const webchatInitialState = {
+ width: WEBCHAT.DEFAULTS.WIDTH,
+ height: WEBCHAT.DEFAULTS.HEIGHT,
+ messagesJSON: [],
+ messagesComponents: [],
+ replies: [],
+ latestInput: {},
+ typing: false,
+ webview: null,
+ webviewParams: null,
+ session: { user: null },
+ lastRoutePath: null,
+ handoff: false,
+ theme: {
+ headerTitle: WEBCHAT.DEFAULTS.TITLE,
+ brandColor: COLORS.BOTONIC_BLUE,
+ brandImage: WEBCHAT.DEFAULTS.LOGO,
+ triggerButtonImage: undefined,
+ textPlaceholder: WEBCHAT.DEFAULTS.PLACEHOLDER,
+ style: {
+ fontFamily: WEBCHAT.DEFAULTS.FONT_FAMILY,
+ },
+ },
+ themeUpdates: {},
+ error: {},
+ online: true,
+ devSettings: { keepSessionOnReload: false },
+ isWebchatOpen: false,
+ isEmojiPickerOpen: false,
+ isPersistentMenuOpen: false,
+ isCoverComponentOpen: false,
+ lastMessageUpdate: undefined,
+ currentAttachment: undefined,
+ jwt: null,
+}
+
+export function useWebchat() {
+ const [webchatState, webchatDispatch] = useReducer(
+ webchatReducer,
+ webchatInitialState
+ )
+
+ const addMessage = message =>
+ webchatDispatch({ type: ADD_MESSAGE, payload: message })
+ const addMessageComponent = message =>
+ webchatDispatch({ type: ADD_MESSAGE_COMPONENT, payload: message })
+ const updateMessage = message =>
+ webchatDispatch({ type: UPDATE_MESSAGE, payload: message })
+ const updateReplies = replies =>
+ webchatDispatch({ type: UPDATE_REPLIES, payload: replies })
+ const updateLatestInput = input =>
+ webchatDispatch({ type: UPDATE_LATEST_INPUT, payload: input })
+ const updateTyping = typing =>
+ webchatDispatch({ type: UPDATE_TYPING, payload: typing })
+ const updateWebview = (webview, params) =>
+ webchatDispatch({
+ type: UPDATE_WEBVIEW,
+ payload: { webview, webviewParams: params },
+ })
+ const updateSession = session => {
+ webchatDispatch({
+ type: UPDATE_SESSION,
+ payload: session,
+ })
+ }
+
+ const updateLastRoutePath = path =>
+ webchatDispatch({
+ type: UPDATE_LAST_ROUTE_PATH,
+ payload: path,
+ })
+ const updateHandoff = handoff =>
+ webchatDispatch({
+ type: UPDATE_HANDOFF,
+ payload: handoff,
+ })
+ const updateTheme = (theme, themeUpdates = undefined) => {
+ const payload =
+ themeUpdates !== undefined ? { theme, themeUpdates } : { theme }
+ webchatDispatch({
+ type: UPDATE_THEME,
+ payload,
+ })
+ }
+ const updateDevSettings = settings =>
+ webchatDispatch({
+ type: UPDATE_DEV_SETTINGS,
+ payload: settings,
+ })
+ const toggleWebchat = toggle =>
+ webchatDispatch({
+ type: TOGGLE_WEBCHAT,
+ payload: toggle,
+ })
+ const toggleEmojiPicker = toggle =>
+ webchatDispatch({
+ type: TOGGLE_EMOJI_PICKER,
+ payload: toggle,
+ })
+ const togglePersistentMenu = toggle =>
+ webchatDispatch({
+ type: TOGGLE_PERSISTENT_MENU,
+ payload: toggle,
+ })
+ const toggleCoverComponent = toggle =>
+ webchatDispatch({
+ type: TOGGLE_COVER_COMPONENT,
+ payload: toggle,
+ })
+ const setError = error =>
+ webchatDispatch({
+ type: SET_ERROR,
+ payload: error,
+ })
+ const setOnline = online =>
+ webchatDispatch({
+ type: SET_ONLINE,
+ payload: online,
+ })
+
+ const clearMessages = () => {
+ webchatDispatch({
+ type: CLEAR_MESSAGES,
+ })
+ }
+ const updateLastMessageDate = date => {
+ webchatDispatch({
+ type: UPDATE_LAST_MESSAGE_DATE,
+ payload: date,
+ })
+ }
+ const setCurrentAttachment = attachment => {
+ webchatDispatch({
+ type: SET_CURRENT_ATTACHMENT,
+ payload: attachment,
+ })
+ }
+
+ const updateJwt = jwt => {
+ webchatDispatch({
+ type: UPDATE_JWT,
+ payload: jwt,
+ })
+ }
+
+ return {
+ webchatState,
+ webchatDispatch,
+ addMessage,
+ addMessageComponent,
+ updateMessage,
+ updateReplies,
+ updateLatestInput,
+ updateTyping,
+ updateWebview,
+ updateSession,
+ updateLastRoutePath,
+ updateHandoff,
+ updateTheme,
+ updateDevSettings,
+ toggleWebchat,
+ toggleEmojiPicker,
+ togglePersistentMenu,
+ toggleCoverComponent,
+ setError,
+ setOnline,
+ clearMessages,
+ updateLastMessageDate,
+ setCurrentAttachment,
+ updateJwt,
+ }
+}
+
+export function useTyping({ webchatState, updateTyping, updateMessage, host }) {
+ useEffect(() => {
+ let delayTimeout, typingTimeout
+ scrollToBottom({ host })
+ try {
+ const nextMsg = webchatState.messagesJSON.filter(m => !m.display)[0]
+ if (nextMsg.delay && nextMsg.typing) {
+ delayTimeout = setTimeout(
+ () => updateTyping(true),
+ nextMsg.delay * 1000
+ )
+ } else if (nextMsg.typing) updateTyping(true)
+ const totalDelay = nextMsg.delay + nextMsg.typing
+ if (totalDelay)
+ typingTimeout = setTimeout(() => {
+ updateMessage({ ...nextMsg, display: true })
+ updateTyping(false)
+ }, totalDelay * 1000)
+ } catch (e) {}
+ return () => {
+ clearTimeout(delayTimeout)
+ clearTimeout(typingTimeout)
+ }
+ }, [webchatState.messagesJSON, webchatState.typing])
+}
+
+export function usePrevious(value) {
+ const ref = useRef()
+ useEffect(() => {
+ ref.current = value
+ })
+ return ref.current
+}
+
+export function useComponentVisible(initialIsVisible, onClickOutside) {
+ const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible)
+ const ref = useRef(null)
+ const handleClickOutside = event => {
+ if (ref.current && !ref.current.contains(event.target)) {
+ setIsComponentVisible(false)
+ onClickOutside()
+ }
+ }
+ useEffect(() => {
+ document.addEventListener('click', handleClickOutside, false)
+ return () => {
+ document.removeEventListener('click', handleClickOutside, false)
+ }
+ })
+ return { ref, isComponentVisible, setIsComponentVisible }
+}
+
+export const useComponentWillMount = func => {
+ useMemo(func, [])
+}
diff --git a/packages/botonic-react/src/experimental/webchat/messages-reducer.js b/packages/botonic-react/src/experimental/webchat/messages-reducer.js
new file mode 100644
index 0000000000..2fbb6c06d9
--- /dev/null
+++ b/packages/botonic-react/src/experimental/webchat/messages-reducer.js
@@ -0,0 +1,86 @@
+import {
+ ADD_MESSAGE,
+ ADD_MESSAGE_COMPONENT,
+ CLEAR_MESSAGES,
+ UPDATE_LAST_MESSAGE_DATE,
+ UPDATE_MESSAGE,
+ UPDATE_REPLIES,
+} from './actions'
+
+export const messagesReducer = (state, action) => {
+ switch (action.type) {
+ case ADD_MESSAGE:
+ return addMessageReducer(state, action)
+ case ADD_MESSAGE_COMPONENT:
+ return {
+ ...state,
+ messagesComponents: [
+ ...(state.messagesComponents || []),
+ action.payload,
+ ],
+ }
+ case UPDATE_MESSAGE:
+ return updateMessageReducer(state, action)
+ case UPDATE_REPLIES:
+ return { ...state, replies: action.payload }
+ case CLEAR_MESSAGES:
+ return {
+ ...state,
+ messagesJSON: [],
+ messagesComponents: [],
+ }
+ case UPDATE_LAST_MESSAGE_DATE:
+ return {
+ ...state,
+ lastMessageUpdate: action.payload,
+ }
+ default:
+ throw new Error()
+ }
+}
+
+function updateMessageReducer(state, action) {
+ const msgIndex = state.messagesJSON.map(m => m.id).indexOf(action.payload.id)
+ if (msgIndex > -1) {
+ const msgComponent = state.messagesComponents[msgIndex]
+ let updatedMessageComponents = {}
+ if (msgComponent) {
+ const updatedMsgComponent = {
+ ...msgComponent,
+ ...{
+ props: { ...msgComponent.props, ack: action.payload.ack },
+ },
+ }
+ updatedMessageComponents = {
+ messagesComponents: [
+ ...state.messagesComponents.slice(0, msgIndex),
+ { ...updatedMsgComponent },
+ ...state.messagesComponents.slice(msgIndex + 1),
+ ],
+ }
+ }
+ return {
+ ...state,
+ messagesJSON: [
+ ...state.messagesJSON.slice(0, msgIndex),
+ { ...action.payload },
+ ...state.messagesJSON.slice(msgIndex + 1),
+ ],
+ ...updatedMessageComponents,
+ }
+ }
+
+ return state
+}
+
+function addMessageReducer(state, action) {
+ if (
+ state.messagesJSON &&
+ state.messagesJSON.find(m => m.id === action.payload.id)
+ )
+ return state
+ return {
+ ...state,
+ messagesJSON: [...(state.messagesJSON || []), action.payload],
+ }
+}
diff --git a/packages/botonic-react/src/experimental/webchat/session-view.jsx b/packages/botonic-react/src/experimental/webchat/session-view.jsx
new file mode 100644
index 0000000000..85d254b703
--- /dev/null
+++ b/packages/botonic-react/src/experimental/webchat/session-view.jsx
@@ -0,0 +1,160 @@
+import React from 'react'
+import JSONTree from 'react-json-tree'
+import styled from 'styled-components'
+
+import { COLORS } from '../constants'
+import { useWebchat } from './hooks'
+
+const AttributeContainer = styled.div`
+ display: flex;
+ flex: none;
+ padding: 12px;
+ padding-bottom: 0px;
+ font-size: 12px;
+ font-weight: 600;
+ color: ${COLORS.SOLID_WHITE};
+ align-items: center;
+`
+
+const Label = styled.div`
+ flex: none;
+`
+
+const Value = styled.div`
+ flex: 1 1 auto;
+ max-height: 20px;
+ font-size: 16px;
+ font-weight: 400;
+ margin-left: 6px;
+ color: ${COLORS.CURIOUS_BLUE};
+ overflow-x: hidden;
+`
+
+const SessionViewAttribute = props => (
+
+
+ {props.value}
+
+)
+
+const SessionViewContent = styled.div`
+ position: relative;
+ width: ${props => (props.show ? '100%' : '0%')};
+ height: 100%;
+ display: flex;
+ background-color: ${COLORS.DAINTREE_BLUE};
+ font-family: Arial, Helvetica, sans-serif;
+ flex-direction: column;
+ z-index: 100000;
+ transition: all 0.2s ease-in-out;
+`
+
+const ToggleTab = styled.div`
+ position: absolute;
+ top: 10px;
+ right: -32px;
+ width: 32px;
+ height: 32px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${COLORS.SOLID_WHITE_ALPHA_0_8};
+ font-size: 14px;
+ font-weight: 600;
+ background-color: ${COLORS.DAINTREE_BLUE};
+ flex-direction: column;
+ z-index: 100000;
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+`
+
+const ContentContainer = styled.div`
+ overflow-y: auto;
+ overflow-x: auto;
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+`
+
+const Title = styled.div`
+ padding: 12px;
+ text-align: center;
+ color: ${COLORS.SOLID_WHITE};
+ border-bottom: 1px solid ${COLORS.SOLID_WHITE_ALPHA_0_2};
+`
+
+const SessionContainer = styled.div`
+ flex: 1 1 auto;
+ height: 100%;
+ overflow-y: auto;
+`
+
+const KeepSessionContainer = styled.div`
+ flex: none;
+ padding: 12px;
+ color: ${COLORS.SOLID_WHITE_ALPHA_0_8};
+ font-size: 12px;
+`
+
+export const SessionView = props => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { webchatState, updateDevSettings } = props.webchatHooks || useWebchat()
+ const { latestInput: input, session, lastRoutePath } = webchatState
+ const toggleSessionView = () =>
+ updateDevSettings({
+ ...webchatState.devSettings,
+ showSessionView: !webchatState.devSettings.showSessionView,
+ })
+ const toggleKeepSessionOnReload = () =>
+ updateDevSettings({
+ ...webchatState.devSettings,
+ keepSessionOnReload: !webchatState.devSettings.keepSessionOnReload,
+ })
+ return (
+
+
+ {webchatState.devSettings.showSessionView ? '⇤' : '⇥'}
+
+
+ Botonic Dev Console
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/botonic-react/src/experimental/webchat/webchat-dev.jsx b/packages/botonic-react/src/experimental/webchat/webchat-dev.jsx
index 2d591ad7c2..57c4b20d37 100644
--- a/packages/botonic-react/src/experimental/webchat/webchat-dev.jsx
+++ b/packages/botonic-react/src/experimental/webchat/webchat-dev.jsx
@@ -5,7 +5,6 @@ import React, { forwardRef, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
-import { useWebchat } from '../../webchat/hooks'
import { SessionView } from '../../webchat/session-view'
import MessengerLogo from './assets/messenger.svg'
import Open from './assets/open.svg'
@@ -13,6 +12,7 @@ import OpenNewWindow from './assets/open-new-window.svg'
import TelegramLogo from './assets/telegram.svg'
import WebchatLogo from './assets/webchat.svg'
import WhatsappLogo from './assets/whatsapp.svg'
+import { useWebchat } from './hooks'
import { Webchat } from './webchat'
export const DebugTab = styled.div`
diff --git a/packages/botonic-react/src/experimental/webchat/webchat-reducer.js b/packages/botonic-react/src/experimental/webchat/webchat-reducer.js
new file mode 100644
index 0000000000..06e9a0e4d4
--- /dev/null
+++ b/packages/botonic-react/src/experimental/webchat/webchat-reducer.js
@@ -0,0 +1,61 @@
+import {
+ SET_CURRENT_ATTACHMENT,
+ SET_ERROR,
+ SET_ONLINE,
+ TOGGLE_COVER_COMPONENT,
+ TOGGLE_EMOJI_PICKER,
+ TOGGLE_PERSISTENT_MENU,
+ TOGGLE_WEBCHAT,
+ UPDATE_DEV_SETTINGS,
+ UPDATE_HANDOFF,
+ UPDATE_JWT,
+ UPDATE_LAST_ROUTE_PATH,
+ UPDATE_LATEST_INPUT,
+ UPDATE_SESSION,
+ UPDATE_THEME,
+ UPDATE_TYPING,
+ UPDATE_WEBVIEW,
+} from './actions'
+import { messagesReducer } from './messages-reducer'
+
+export function webchatReducer(state, action) {
+ switch (action.type) {
+ case UPDATE_WEBVIEW:
+ return { ...state, ...action.payload }
+ case UPDATE_SESSION:
+ return { ...state, session: { ...action.payload } }
+ case UPDATE_TYPING:
+ return { ...state, typing: action.payload }
+ case UPDATE_THEME:
+ return {
+ ...state,
+ ...action.payload,
+ }
+ case UPDATE_HANDOFF:
+ return { ...state, handoff: action.payload }
+ case TOGGLE_WEBCHAT:
+ return { ...state, isWebchatOpen: action.payload }
+ case TOGGLE_EMOJI_PICKER:
+ return { ...state, isEmojiPickerOpen: action.payload }
+ case TOGGLE_PERSISTENT_MENU:
+ return { ...state, isPersistentMenuOpen: action.payload }
+ case TOGGLE_COVER_COMPONENT:
+ return { ...state, isCoverComponentOpen: action.payload }
+ case SET_ERROR:
+ return { ...state, error: action.payload || {} }
+ case SET_ONLINE:
+ return { ...state, online: action.payload }
+ case UPDATE_DEV_SETTINGS:
+ return { ...state, devSettings: { ...action.payload } }
+ case UPDATE_LATEST_INPUT:
+ return { ...state, latestInput: action.payload }
+ case UPDATE_LAST_ROUTE_PATH:
+ return { ...state, lastRoutePath: action.payload }
+ case SET_CURRENT_ATTACHMENT:
+ return { ...state, currentAttachment: action.payload }
+ case UPDATE_JWT:
+ return { ...state, jwt: action.payload }
+ default:
+ return messagesReducer(state, action)
+ }
+}
diff --git a/packages/botonic-react/src/experimental/webchat/webchat.jsx b/packages/botonic-react/src/experimental/webchat/webchat.jsx
index 31385fdeff..9694184206 100644
--- a/packages/botonic-react/src/experimental/webchat/webchat.jsx
+++ b/packages/botonic-react/src/experimental/webchat/webchat.jsx
@@ -230,6 +230,8 @@ export const Webchat = forwardRef((props, ref) => {
stringifyWithRegexs({
messages: webchatState.messagesJSON,
session: webchatState.session,
+ botState: webchatState.botState,
+ user: webchatState.user,
lastRoutePath: webchatState.lastRoutePath,
devSettings: webchatState.devSettings,
lastMessageUpdate: webchatState.lastMessageUpdate,