@botonic/react
Version:
Build Chatbots using React
512 lines • 26.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.Webchat = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const core_1 = require("@botonic/core");
const lodash_merge_1 = tslib_1.__importDefault(require("lodash.merge"));
const react_1 = require("react");
const styled_components_1 = require("styled-components");
const uuid_1 = require("uuid");
const components_1 = require("../components");
const constants_1 = require("../constants");
const index_types_1 = require("../index-types");
const message_utils_1 = require("../message-utils");
const msg_to_botonic_1 = require("../msg-to-botonic");
const environment_1 = require("../util/environment");
const regexs_1 = require("../util/regexs");
const webchat_1 = require("../util/webchat");
const chat_area_1 = require("./chat-area");
const opened_persistent_menu_1 = require("./components/opened-persistent-menu");
const constants_2 = require("./constants");
const context_1 = require("./context");
const cover_component_1 = require("./cover-component");
const header_1 = require("./header");
const hooks_1 = require("./hooks");
const input_panel_1 = require("./input-panel");
const styles_1 = require("./styles");
const trigger_button_1 = require("./trigger-button");
const use_storage_state_hook_1 = require("./use-storage-state-hook");
const utils_1 = require("./utils");
const index_1 = require("./webview/index");
// eslint-disable-next-line complexity, react/display-name
const Webchat = (0, react_1.forwardRef)((props, ref) => {
var _a;
const { addMessage, addMessageComponent, clearMessages, doRenderCustomComponent, resetUnreadMessages, setCurrentAttachment, setError, setIsInputFocused, setLastMessageVisible, setOnline, toggleCoverComponent, toggleEmojiPicker, togglePersistentMenu, toggleWebchat, updateDevSettings, updateHandoff, updateLastMessageDate, updateLastRoutePath, updateLatestInput, updateMessage, updateReplies, updateSession, updateTheme, updateTyping, updateWebview, removeWebview, removeReplies, webchatState, webchatContainerRef, chatAreaRef, inputPanelRef, headerRef, repliesRef, scrollableMessagesListRef,
// eslint-disable-next-line react-hooks/rules-of-hooks
} = props.webchatHooks || (0, context_1.useWebchat)(props.theme);
const firstUpdate = (0, react_1.useRef)(true);
const isOnline = () => webchatState.online;
const currentDateString = () => new Date().toISOString();
const theme = (0, lodash_merge_1.default)(webchatState.theme, props.theme);
const { initialSession, initialDevSettings, onStateChange } = props;
const getThemeProperty = (0, webchat_1._getThemeProperty)(theme);
const [customComponent, setCustomComponent] = (0, react_1.useState)(null);
const storage = props.storage;
const storageKey = typeof props.storageKey === 'function'
? props.storageKey()
: props.storageKey;
const [botonicState, saveState] = (0, use_storage_state_hook_1.useStorageState)(storage, storageKey);
const host = props.host || document.body;
const { scrollToBottom } = (0, hooks_1.useScrollToBottom)({ host });
const saveWebchatState = (webchatState) => {
storage &&
saveState(JSON.parse((0, regexs_1.stringifyWithRegexs)({
messages: webchatState.messagesJSON,
session: webchatState.session,
lastRoutePath: webchatState.lastRoutePath,
devSettings: webchatState.devSettings,
lastMessageUpdate: webchatState.lastMessageUpdate,
themeUpdates: webchatState.themeUpdates,
})));
};
const handleAttachment = (event) => {
if (!(0, message_utils_1.isAllowedSize)(event.target.files[0].size)) {
throw new Error(`The file is too large. A maximum of ${constants_1.MAX_ALLOWED_SIZE_MB}MB is allowed.`);
}
// TODO: Attach more files?
setCurrentAttachment(event.target.files[0]);
};
(0, react_1.useEffect)(() => {
if (webchatState.currentAttachment) {
sendAttachment(webchatState.currentAttachment);
}
}, [webchatState.currentAttachment]);
const sendUserInput = (input) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (props.onUserInput) {
resetUnreadMessages();
scrollToBottom();
props.onUserInput({
user: webchatState.session.user,
// TODO: Review if this input.sentBy exists in the frontend
input: input,
//@ts-ignore
session: webchatState.session,
// TODO: Review why we were passing lastRoutePath, is only for devMode?
lastRoutePath: webchatState.lastRoutePath,
});
}
});
// Load styles stored in window._botonicInsertStyles by Webpack
(0, hooks_1.useComponentWillMount)(() => {
if (window._botonicInsertStyles && window._botonicInsertStyles.length) {
for (const botonicStyle of window._botonicInsertStyles) {
// Injecting styles at head is needed even if we use shadowDOM
// as some dependencies like simplebar rely on creating ephemeral elements
// on document.body and assume styles will be available globally
document.head.appendChild(botonicStyle);
// injecting styles in host node too so that shadowDOM works
if (props.shadowDOM)
host.appendChild(botonicStyle.cloneNode(true));
}
delete window._botonicInsertStyles;
}
if (props.shadowDOM) {
// emoji-picker-react injects styles in head, so we need to
// re-inject them in our host node to make it work with shadowDOM
for (const style of document.querySelectorAll('style')) {
if (style.textContent &&
style.textContent.includes('emoji-picker-react'))
host.appendChild(style.cloneNode(true));
}
}
});
// Load initial state from storage
(0, react_1.useEffect)(() => {
let { messages, session, lastRoutePath, devSettings, lastMessageUpdate, themeUpdates, } = botonicState || {};
session = (0, webchat_1.initSession)(session);
updateSession(session);
if ((0, webchat_1.shouldKeepSessionOnReload)({ initialDevSettings, devSettings })) {
if (messages) {
messages.forEach(message => {
var _a, _b;
addMessage(message);
const newMessageComponent = (0, msg_to_botonic_1.msgToBotonic)(Object.assign(Object.assign({}, message), { delay: 0, typing: 0 }), (_b = (_a = props.theme) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.customTypes);
//@ts-ignore
if (newMessageComponent)
addMessageComponent(newMessageComponent);
});
}
if (initialSession)
updateSession((0, lodash_merge_1.default)(initialSession, session));
if (lastRoutePath)
updateLastRoutePath(lastRoutePath);
}
else
updateSession((0, lodash_merge_1.default)(initialSession, session));
if (devSettings)
updateDevSettings(devSettings);
else if (initialDevSettings)
updateDevSettings(initialDevSettings);
if (lastMessageUpdate) {
updateLastMessageDate(lastMessageUpdate);
}
if (themeUpdates !== undefined) {
updateTheme((0, lodash_merge_1.default)(props.theme, themeUpdates), themeUpdates);
}
if (props.onInit) {
setTimeout(() => {
if (typeof props.onInit === 'function') {
props.onInit();
session.user = (0, webchat_1.updateUserLocaleAndCountry)(session.user);
}
}, 100);
}
}, []);
(0, react_1.useEffect)(() => {
if (!webchatState.isWebchatOpen) {
if (webchatState.isLastMessageVisible) {
resetUnreadMessages();
}
return;
}
}, [webchatState.isWebchatOpen]);
(0, react_1.useEffect)(() => {
const { messagesJSON, session } = webchatState;
if (onStateChange && typeof onStateChange === 'function' && session.user) {
onStateChange({ messagesJSON, user: session.user });
}
saveWebchatState(webchatState);
}, [
webchatState.messagesJSON,
webchatState.session,
webchatState.lastRoutePath,
webchatState.devSettings,
webchatState.lastMessageUpdate,
]);
(0, react_1.useEffect)(() => {
if (!webchatState.online) {
setError({
message: (0, webchat_1.getServerErrorMessage)(props.server),
});
}
else {
if (!firstUpdate.current) {
setError(undefined);
}
}
}, [webchatState.online]);
(0, hooks_1.useTyping)({ webchatState, updateTyping, updateMessage, host });
(0, react_1.useEffect)(() => {
updateTheme((0, lodash_merge_1.default)(props.theme, theme, webchatState.themeUpdates));
}, [props.theme, webchatState.themeUpdates]);
const openWebview = (webviewComponent, params) => {
updateWebview(webviewComponent, params);
};
const textareaRef = (0, react_1.useRef)();
const closeWebview = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
var _b;
removeWebview();
if (userInputEnabled) {
(_b = textareaRef.current) === null || _b === void 0 ? void 0 : _b.focus();
}
if (options === null || options === void 0 ? void 0 : options.payload) {
yield sendPayload(options.payload);
}
else if (options === null || options === void 0 ? void 0 : options.path) {
const params = options.params ? (0, core_1.params2queryString)(options.params) : '';
yield sendPayload(`__PATH_PAYLOAD__${options.path}?${params}`);
}
});
const persistentMenuOptions = getThemeProperty(constants_1.WEBCHAT.CUSTOM_PROPERTIES.persistentMenu);
const darkBackgroundMenu = getThemeProperty(constants_1.WEBCHAT.CUSTOM_PROPERTIES.darkBackgroundMenu);
const getBlockInputs = (rule, inputData) => {
const processedInput = rule.preprocess
? rule.preprocess(inputData)
: inputData;
return rule.match.some(regex => {
if (typeof regex === 'string')
regex = (0, regexs_1.deserializeRegex)(regex);
return regex.test(processedInput);
});
};
const checkBlockInput = input => {
var _a;
// if is a text we check if it is a serialized RE
const blockInputs = (_a = webchatState.theme.userInput) === null || _a === void 0 ? void 0 : _a.blockInputs;
if (!Array.isArray(blockInputs))
return false;
for (const rule of blockInputs) {
if (getBlockInputs(rule, input.data)) {
addMessageComponent((0, jsx_runtime_1.jsx)(components_1.Text
// Is necessary to add the id of the input
// to keep the input.id generated in the frontend as id of the message
// @ts-ignore
, Object.assign({
// Is necessary to add the id of the input
// to keep the input.id generated in the frontend as id of the message
// @ts-ignore
id: input.id, sentBy: index_types_1.SENDERS.user, blob: false, style: {
backgroundColor: constants_1.COLORS.SCORPION_GRAY,
borderColor: constants_1.COLORS.SCORPION_GRAY,
padding: '8px 12px',
} }, { children: rule.message })));
removeReplies();
return true;
}
}
return false;
};
const closeMenu = () => {
togglePersistentMenu(false);
};
const persistentMenu = () => {
return ((0, jsx_runtime_1.jsx)(opened_persistent_menu_1.OpenedPersistentMenu, { onClick: closeMenu, options: persistentMenuOptions, borderRadius: webchatState.theme.style.borderRadius || '10px' }));
};
const coverComponent = webchatState.theme.coverComponent;
const coverComponentProps = (_a = webchatState.theme.coverComponent) === null || _a === void 0 ? void 0 : _a.props;
(0, react_1.useEffect)(() => {
if (!coverComponent)
return;
if (!botonicState ||
(botonicState.messages && botonicState.messages.length === 0))
toggleCoverComponent(true);
}, []);
const messageComponentFromInput = input => {
let messageComponent = null;
if ((0, message_utils_1.isText)(input)) {
messageComponent = ((0, jsx_runtime_1.jsx)(components_1.Text
// Is necessary to add the id of the input
// to keep the input.id generated in the frontend as id of the message
// @ts-ignore
, Object.assign({
// Is necessary to add the id of the input
// to keep the input.id generated in the frontend as id of the message
// @ts-ignore
id: input.id,
// Is necessary to add the payload of the input when user clicks a button
// @ts-ignore
payload: input.payload, sentBy: index_types_1.SENDERS.user }, { children: input.data })));
}
else if ((0, message_utils_1.isMedia)(input)) {
const temporaryDisplayUrl = URL.createObjectURL(input.data);
// TODO: We should use URL.revokeObjectURL(temporaryDisplayUrl) when the component is unmounted
// https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static#memory_management
const mediaProps = {
id: input.id,
sentBy: index_types_1.SENDERS.user,
src: temporaryDisplayUrl,
};
if ((0, message_utils_1.isImage)(input)) {
mediaProps.input = input;
messageComponent = (0, jsx_runtime_1.jsx)(components_1.Image, Object.assign({}, mediaProps));
}
else if ((0, message_utils_1.isAudio)(input))
messageComponent = (0, jsx_runtime_1.jsx)(components_1.Audio, Object.assign({}, mediaProps));
else if ((0, message_utils_1.isVideo)(input))
messageComponent = (0, jsx_runtime_1.jsx)(components_1.Video, Object.assign({}, mediaProps));
else if ((0, message_utils_1.isDocument)(input))
messageComponent = (0, jsx_runtime_1.jsx)(components_1.Document, Object.assign({}, mediaProps));
}
return messageComponent;
};
const sendInput = (input) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!input || Object.keys(input).length == 0)
return;
if ((0, message_utils_1.isText)(input) && (!input.data || !input.data.trim()))
return; // in case trim() doesn't work in a browser we can use !/\S/.test(input.data)
if ((0, message_utils_1.isText)(input) && checkBlockInput(input))
return;
if (!input.id)
input.id = (0, uuid_1.v7)();
const messageComponent = messageComponentFromInput(input);
if (messageComponent)
addMessageComponent(messageComponent);
if ((0, message_utils_1.isMedia)(input))
input.data = yield (0, message_utils_1.readDataURL)(input.data);
sendUserInput(input);
updateLatestInput(input);
isOnline() && updateLastMessageDate(currentDateString());
removeReplies();
togglePersistentMenu(false);
toggleEmojiPicker(false);
});
/* This is the public API this component exposes to its parents
https://stackoverflow.com/questions/37949981/call-child-method-from-parent
*/
const updateSessionWithUser = (userToUpdate) => {
console.log('userToUpdate', userToUpdate);
updateSession((0, lodash_merge_1.default)(webchatState.session, { user: userToUpdate }));
};
(0, react_1.useImperativeHandle)(ref, () => ({
addBotResponse: ({ response, session, lastRoutePath }) => {
updateTyping(false);
const isUnread = !webchatState.isLastMessageVisible || webchatState.numUnreadMessages > 0;
if (Array.isArray(response)) {
response.forEach(r => {
addMessageComponent(Object.assign(Object.assign({}, r), { props: Object.assign(Object.assign({}, r.props), { isUnread }) }));
});
}
else if (response) {
addMessageComponent(Object.assign(Object.assign({}, response), { props: Object.assign(Object.assign({}, response.props), { isUnread }) }));
}
if (session) {
updateSession((0, lodash_merge_1.default)(session, { user: webchatState.session.user }));
const action = session._botonic_action || '';
const handoff = action.startsWith(core_1.BotonicAction.CreateCase);
if (handoff && environment_1.isDev)
addMessageComponent((0, jsx_runtime_1.jsx)(components_1.Handoff, {}));
updateHandoff(handoff);
}
if (lastRoutePath)
updateLastRoutePath(lastRoutePath);
updateLastMessageDate(currentDateString());
},
setTyping: (typing) => updateTyping(typing),
addUserMessage: message => sendInput(message),
updateUser: updateSessionWithUser,
openWebchat: () => toggleWebchat(true),
closeWebchat: () => toggleWebchat(false),
toggleWebchat: () => toggleWebchat(!webchatState.isWebchatOpen),
openCoverComponent: () => toggleCoverComponent(true),
closeCoverComponent: () => toggleCoverComponent(false),
renderCustomComponent: _customComponent => {
setCustomComponent(_customComponent);
doRenderCustomComponent(true);
},
unmountCustomComponent: () => doRenderCustomComponent(false),
toggleCoverComponent: () => toggleCoverComponent(!webchatState.isCoverComponentOpen),
setOnline,
getMessages: () => webchatState.messagesJSON,
isOnline,
clearMessages: () => {
clearMessages();
removeReplies();
},
getLastMessageUpdate: () => webchatState.lastMessageUpdate,
updateMessageInfo: (msgId, messageInfo) => {
const messageToUpdate = webchatState.messagesJSON.filter(m => m.id === msgId)[0];
const updatedMsg = (0, lodash_merge_1.default)(messageToUpdate, messageInfo);
if (updatedMsg.ack === 1)
delete updatedMsg.unsentInput;
updateMessage(updatedMsg);
},
updateWebchatSettings: (settings) => {
if (settings.user) {
updateSessionWithUser(settings.user);
}
const themeUpdates = (0, components_1.normalizeWebchatSettings)(settings);
updateTheme((0, lodash_merge_1.default)(webchatState.theme, themeUpdates), themeUpdates);
updateTyping(false);
},
closeWebview: (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { return closeWebview(options); }),
}));
const resolveCase = () => {
updateHandoff(false);
updateSession(Object.assign(Object.assign({}, webchatState.session), { _botonic_action: undefined }));
};
const prevSession = (0, hooks_1.usePrevious)(webchatState.session);
(0, react_1.useEffect)(() => {
// Resume conversation after handoff
if ((prevSession === null || prevSession === void 0 ? void 0 : prevSession._botonic_action) && !webchatState.session._botonic_action) {
const action = (0, utils_1.getParsedAction)(prevSession._botonic_action);
if (action === null || action === void 0 ? void 0 : action.on_finish)
sendPayload(action.on_finish);
}
}, [webchatState.session._botonic_action]);
const sendText = (text, payload) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!text)
return;
const input = { type: core_1.INPUT.TEXT, data: text, payload };
yield sendInput(input);
});
const sendPayload = (payload) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!payload)
return;
const input = { type: core_1.INPUT.POSTBACK, payload };
yield sendInput(input);
});
const sendAttachment = (attachment) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (attachment) {
const attachmentType = (0, message_utils_1.getMediaType)(attachment.type);
if (!attachmentType)
return;
const input = {
type: attachmentType,
data: attachment,
};
yield sendInput(input);
setCurrentAttachment();
}
});
(0, react_1.useEffect)(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
if (webchatState.isWebchatOpen && props.onOpen)
props.onOpen();
if (!webchatState.isWebchatOpen && props.onClose && !firstUpdate.current) {
props.onClose();
toggleEmojiPicker(false);
togglePersistentMenu(false);
}
}, [webchatState.isWebchatOpen]);
const isUserInputEnabled = () => {
const isUserInputEnabled = getThemeProperty(constants_1.WEBCHAT.CUSTOM_PROPERTIES.enableUserInput);
return isUserInputEnabled && !webchatState.isCoverComponentOpen;
};
const userInputEnabled = isUserInputEnabled();
(0, react_1.useEffect)(() => {
// Prod mode
saveWebchatState(webchatState);
}, [webchatState.themeUpdates]);
// Only needed for dev/serve mode
const updateWebchatDevSettings = settings => {
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, react_1.useEffect)(() => {
const themeUpdates = (0, components_1.normalizeWebchatSettings)(settings);
updateTheme((0, lodash_merge_1.default)(webchatState.theme, themeUpdates), themeUpdates);
}, [webchatState.messagesJSON]);
};
const DarkenBackground = ({ component }) => {
return ((0, jsx_runtime_1.jsxs)("div", { children: [darkBackgroundMenu && ((0, jsx_runtime_1.jsx)(styles_1.DarkBackgroundMenu, { style: {
borderRadius: webchatState.theme.style.borderRadius,
} })), component] }));
};
const _renderCustomComponent = () => {
if (!customComponent)
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
else
return customComponent;
};
const WebchatComponent = ((0, jsx_runtime_1.jsx)(context_1.WebchatContext.Provider, Object.assign({ value: {
addMessage,
getThemeProperty,
closeWebview,
openWebview,
resolveCase,
resetUnreadMessages,
setIsInputFocused,
setLastMessageVisible,
sendAttachment,
sendInput,
sendPayload,
sendText,
toggleWebchat,
toggleEmojiPicker,
togglePersistentMenu,
toggleCoverComponent,
updateLatestInput,
updateMessage,
updateReplies,
updateUser: updateSessionWithUser,
updateWebchatDevSettings: updateWebchatDevSettings,
trackEvent: props.onTrackEvent,
webchatState,
webchatContainerRef,
chatAreaRef,
inputPanelRef,
headerRef,
repliesRef,
scrollableMessagesListRef,
} }, { children: (0, jsx_runtime_1.jsxs)(styled_components_1.ThemeProvider, Object.assign({ theme: webchatState.theme }, { children: [!webchatState.isWebchatOpen && (0, jsx_runtime_1.jsx)(trigger_button_1.TriggerButton, {}), webchatState.isWebchatOpen && ((0, jsx_runtime_1.jsxs)(styles_1.StyledWebchat, Object.assign({ id: constants_2.BotonicContainerId.Webchat, ref: webchatContainerRef,
// TODO: Distinguish between multiple instances of webchat, e.g. `${uniqueId}-botonic-webchat`
role: constants_1.ROLES.WEBCHAT }, { children: [(0, jsx_runtime_1.jsx)(header_1.WebchatHeader, { ref: headerRef }), webchatState.isCoverComponentOpen ? ((0, jsx_runtime_1.jsx)(cover_component_1.CoverComponent, { component: coverComponent, componentProps: coverComponentProps })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [webchatState.error.message && ((0, jsx_runtime_1.jsx)(styles_1.ErrorMessageContainer, { children: (0, jsx_runtime_1.jsx)(styles_1.ErrorMessage, { children: webchatState.error.message }) })), (0, jsx_runtime_1.jsx)(chat_area_1.ChatArea, {}), webchatState.isPersistentMenuOpen && ((0, jsx_runtime_1.jsx)(DarkenBackground, { component: persistentMenu() })), !webchatState.handoff && userInputEnabled && ((0, jsx_runtime_1.jsx)(input_panel_1.InputPanel, { handleAttachment: handleAttachment, textareaRef: textareaRef, host: host, onUserInput: props.onUserInput })), webchatState.webview && (0, jsx_runtime_1.jsx)(index_1.WebviewContainer, {}), webchatState.isCustomComponentRendered &&
customComponent &&
_renderCustomComponent()] }))] })))] })) })));
return props.shadowDOM ? ((0, jsx_runtime_1.jsx)(styled_components_1.StyleSheetManager, Object.assign({ target: host }, { children: WebchatComponent }))) : (WebchatComponent);
});
exports.Webchat = Webchat;
Webchat.displayName = 'Webchat';
//# sourceMappingURL=webchat.js.map
;