@botonic/react
Version:
Build Chatbots using React
296 lines (273 loc) • 9.19 kB
JSX
import { INPUT, isBrowser } from '@botonic/core'
import React, { useContext, useEffect, useState } from 'react'
import { v7 as uuidv7 } from 'uuid'
import { COLORS, WEBCHAT } from '../../constants'
import { RequestContext } from '../../contexts'
import { SENDERS } from '../../index-types'
import { Fade } from '../../shared/styles'
import { isDev } from '../../util/environment'
import { ConditionalWrapper, renderComponent } from '../../util/react'
import { WebchatContext } from '../../webchat/context'
import { Button } from '../button/index'
import { ButtonsDisabler } from '../buttons-disabler'
import { getMarkdownStyle, renderLinks, renderMarkdown } from '../markdown'
import { Reply } from '../reply'
import { MessageFooter } from './message-footer'
import { MessageImage } from './message-image'
import {
BlobContainer,
BlobText,
BlobTick,
BlobTickContainer,
MessageContainer,
} from './styles'
import { resolveMessageTimestamps } from './timestamps'
export const Message = props => {
const { defaultTyping, defaultDelay } = useContext(RequestContext)
let {
type = '',
blob = true,
sentBy,
delay = defaultDelay,
typing = defaultTyping,
children,
enabletimestamps = props.enabletimestamps || props.enableTimestamps,
json,
style,
imagestyle = props.imagestyle || props.imageStyle,
isUnread = true,
feedbackEnabled,
inferenceId,
botInteractionId,
...otherProps
} = props
const isSentByUser = sentBy === SENDERS.user
const isSentByBot = sentBy === SENDERS.bot
const markdown = props.markdown
const { webchatState, addMessage, updateReplies, getThemeProperty } =
useContext(WebchatContext)
const [state] = useState({
id: props.id || uuidv7(),
})
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 (isSentByUser)
textChildren = textChildren.map(e =>
typeof e === 'string' ? renderLinks(e) : e
)
const { timestampsEnabled, getFormattedTimestamp } = resolveMessageTimestamps(
getThemeProperty,
enabletimestamps
)
const getEnvAck = () => {
if (isDev) return 1
if (!isSentByUser) 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,
sentBy,
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,
isUnread: isUnread === 1 || isUnread === true,
feedbackEnabled,
inferenceId,
botInteractionId,
}
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)
const getBgColor = () => {
if (!blob) return COLORS.TRANSPARENT
if (isSentByUser) {
return getThemeProperty(
WEBCHAT.CUSTOM_PROPERTIES.userMessageBackground,
brandColor
)
}
return getThemeProperty(
WEBCHAT.CUSTOM_PROPERTIES.botMessageBackground,
COLORS.SEASHELL_WHITE
)
}
const getMessageStyle = () =>
isSentByUser
? getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.userMessageStyle)
: getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.botMessageStyle)
const userOrBotMessage = isSentByUser ? SENDERS.user : SENDERS.bot
const hasBlobTick = () =>
getThemeProperty(`message.${userOrBotMessage}.blobTick`)
const renderBrowser = () => {
const messageJSON = webchatState.messagesJSON.find(m => m.id === state.id)
if (!messageJSON || !messageJSON.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.${userOrBotMessage}.style.borderColor`,
COLORS.TRANSPARENT
)
const containerStyle = {
...getThemeProperty(`message.${userOrBotMessage}.blobTickStyle`),
}
const blobTickStyle = {}
if (isSentByUser) {
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 animationsEnabled = getThemeProperty(
WEBCHAT.CUSTOM_PROPERTIES.enableAnimations
)
const resolveCustomTypeName = () =>
isSentByBot && type === INPUT.CUSTOM
? ` ${messageJSON.customTypeName}`
: ''
const className = `${type}-${userOrBotMessage}${resolveCustomTypeName()}`
return (
<ConditionalWrapper
condition={animationsEnabled}
wrapper={children => <Fade>{children}</Fade>}
>
<>
<MessageContainer
isSentByUser={isSentByUser}
style={{
...getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.messageStyle),
}}
>
<MessageImage imagestyle={imagestyle} sentBy={sentBy} />
<BlobContainer
className={className}
bgcolor={getBgColor()}
color={isSentByUser ? 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,
isSentByUser ? 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)}
</BlobContainer>
</MessageContainer>
{timestampsEnabled || feedbackEnabled ? (
<MessageFooter
enabletimestamps={timestampsEnabled}
messageJSON={messageJSON}
sentBy={sentBy}
feedbackEnabled={feedbackEnabled}
inferenceId={inferenceId}
botInteractionId={botInteractionId}
/>
) : null}
</>
</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 })
}