UNPKG

@botonic/react

Version:

Build Chatbots using React

345 lines (319 loc) 10.3 kB
import { INPUT, isBrowser } from '@botonic/core' import React, { useContext, useEffect, useState } from 'react' import Fade from 'react-reveal/Fade' import styled from 'styled-components' import { v4 as uuidv4 } from 'uuid' import { COLORS, SENDERS, WEBCHAT } from '../constants' import { RequestContext, WebchatContext } from '../contexts' import { isDev, resolveImage } from '../util/environment' import { ConditionalWrapper, renderComponent } from '../util/react' import { Button } from './button' import { ButtonsDisabler } from './buttons-disabler' import { getMarkdownStyle, renderLinks, renderMarkdown } from './markdown' import { Reply } from './reply' import { MessageTimestamp, resolveMessageTimestamps } from './timestamps' const MessageContainer = styled.div` display: flex; justify-content: ${props => (props.isfromuser ? 'flex-end' : 'flex-start')}; position: relative; padding: 0px 6px; ` const BotMessageImageContainer = styled.div` width: 28px; padding: 12px 4px; flex: none; display: flex; align-items: center; justify-content: center; ` const Blob = styled.div` position: relative; margin: 8px; border-radius: 8px; background-color: ${props => props.bgcolor}; color: ${props => props.color}; max-width: ${props => props.blob ? props.blobwidth ? props.blobwidth : '60%' : 'calc(100% - 16px)'}; ` const BlobText = styled.div` padding: ${props => (props.blob ? '8px 12px' : '0px')}; display: flex; flex-direction: column; white-space: pre-line; ${props => props.markdownstyle} ` const BlobTickContainer = styled.div` position: absolute; box-sizing: border-box; height: 100%; padding: 18px 0px 18px 0px; display: flex; top: 0; align-items: center; ` const BlobTick = styled.div` position: relative; margin: -${props => props.pointerSize}px 0px; border: ${props => props.pointerSize}px solid ${COLORS.TRANSPARENT}; ` export const Message = props => { const { defaultTyping, defaultDelay } = useContext(RequestContext) let { type = '', blob = true, from = SENDERS.bot, delay = defaultDelay, typing = defaultTyping, children, enabletimestamps = props.enabletimestamps || props.enableTimestamps, json, style, imagestyle = props.imagestyle || props.imageStyle, ...otherProps } = props const isFromUser = from === SENDERS.user const isFromBot = from === SENDERS.bot const markdown = props.markdown const { webchatState, addMessage, updateReplies, getThemeProperty } = useContext(WebchatContext) const [state, setState] = useState({ id: props.id || uuidv4(), }) const [disabled, setDisabled] = useState(false) children = ButtonsDisabler.updateChildrenButtons(children, { parentId: state.id, disabled, setDisabled, }) const replies = React.Children.toArray(children).filter(e => e.type === Reply) const buttons = React.Children.toArray(children).filter( e => e.type === Button ) let textChildren = React.Children.toArray(children).filter( e => ![Button, Reply].includes(e.type) ) if (isFromUser) textChildren = textChildren.map(e => typeof e === 'string' ? renderLinks(e) : e ) const { timestampsEnabled, getFormattedTimestamp, timestampStyle } = resolveMessageTimestamps(getThemeProperty, enabletimestamps) const getEnvAck = () => { if (isDev) return 1 if (!isFromUser) return 1 if (props.ack !== undefined) return props.ack return 0 } const ack = getEnvAck() useEffect(() => { if (isBrowser()) { const decomposedChildren = json const message = { id: state.id, type, data: decomposedChildren ? decomposedChildren : textChildren, timestamp: props.timestamp || getFormattedTimestamp, markdown, from, buttons: buttons.map(b => ({ parentId: b.props.parentId, payload: b.props.payload, path: b.props.path, url: b.props.url, target: b.props.target, webview: b.props.webview && String(b.props.webview), title: b.props.children, ...ButtonsDisabler.withDisabledProps(b.props), })), delay, typing, replies: replies.map(r => ({ payload: r.props.payload, path: r.props.path, url: r.props.url, text: r.props.children, })), display: delay + typing == 0, customTypeName: decomposedChildren.customTypeName, ack: ack, } addMessage(message) } }, []) useEffect(() => { if (isBrowser()) { const msg = webchatState.messagesJSON.find(m => m.id === state.id) if ( msg && msg.display && webchatState.messagesJSON.filter(m => !m.display).length == 0 ) { updateReplies(replies) } } }, [webchatState.messagesJSON]) const brandColor = getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.brandColor, COLORS.BOTONIC_BLUE ) const getBgColor = () => { if (!blob) return COLORS.TRANSPARENT if (isFromUser) { return getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.userMessageBackground, brandColor ) } return getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.botMessageBackground, COLORS.SEASHELL_WHITE ) } const getMessageStyle = () => isFromBot ? getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.botMessageStyle) : getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.userMessageStyle) const hasBlobTick = () => getThemeProperty(`message.${from}.blobTick`, true) const renderBrowser = () => { const m = webchatState.messagesJSON.find(m => m.id === state.id) if (!m || !m.display) return <></> const getBlobTick = pointerSize => { // to add a border to the blobTick we need to create two triangles and overlap them // that is why the color depends on the pointerSize // https://developpaper.com/realization-code-of-css-drawing-triangle-border-method/ const color = pointerSize == 5 ? getBgColor() : getThemeProperty( `message.${from}.style.borderColor`, COLORS.TRANSPARENT ) const containerStyle = { ...getThemeProperty(`message.${from}.blobTickStyle`), } const blobTickStyle = {} if (isFromUser) { containerStyle.right = 0 containerStyle.marginRight = -pointerSize blobTickStyle.borderRight = 0 blobTickStyle.borderLeftColor = color } else { containerStyle.left = 0 containerStyle.marginLeft = -pointerSize blobTickStyle.borderLeft = 0 blobTickStyle.borderRightColor = color } return ( <BlobTickContainer style={containerStyle}> <BlobTick pointerSize={pointerSize} style={blobTickStyle} /> </BlobTickContainer> ) } const BotMessageImage = getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.botMessageImage, getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.brandImage, WEBCHAT.DEFAULTS.LOGO ) ) const animationsEnabled = getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.enableAnimations, true ) const resolveCustomTypeName = () => isFromBot && type === INPUT.CUSTOM ? ` ${m.customTypeName}` : '' const className = `${type}-${from}${resolveCustomTypeName()}` return ( <ConditionalWrapper condition={animationsEnabled} wrapper={children => <Fade>{children}</Fade>} > <> <MessageContainer isfromuser={isFromUser} style={{ ...getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.messageStyle), }} > {isFromBot && BotMessageImage && ( <BotMessageImageContainer style={{ ...getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.botMessageImageStyle ), ...imagestyle, }} > <img style={{ width: '100%' }} src={resolveImage(BotMessageImage)} /> </BotMessageImageContainer> )} <Blob className={className} bgcolor={getBgColor()} color={isFromUser ? COLORS.SOLID_WHITE : COLORS.SOLID_BLACK} blobwidth={getThemeProperty( WEBCHAT.CUSTOM_PROPERTIES.botMessageBlobWidth )} blob={blob} style={{ ...getMessageStyle(), ...style, ...{ opacity: ack === 0 ? 0.6 : 1 }, }} {...otherProps} > {markdown ? ( <BlobText blob={blob} dangerouslySetInnerHTML={{ __html: renderMarkdown(textChildren), }} markdownstyle={getMarkdownStyle( getThemeProperty, isFromUser ? COLORS.SEASHELL_WHITE : brandColor )} /> ) : ( <BlobText blob={blob}>{textChildren}</BlobText> )} {!!buttons.length && ( <div className='message-buttons-container'>{buttons}</div> )} {Boolean(blob) && hasBlobTick() && getBlobTick(6)} {Boolean(blob) && hasBlobTick() && getBlobTick(5)} </Blob> </MessageContainer> {timestampsEnabled && ( <MessageTimestamp timestamp={m.timestamp} style={timestampStyle} isfromuser={isFromUser} /> )} </> </ConditionalWrapper> ) } const { blob: _blob, json: _json, ...nodeProps } = props const renderNode = () => type === INPUT.CUSTOM ? ( <message json={JSON.stringify(_json)} typing={typing} delay={delay} {...nodeProps} /> ) : ( <message typing={typing} delay={delay} {...nodeProps}> {children} </message> ) return renderComponent({ renderBrowser, renderNode }) }