# Development Guide ## How To **[Using Bot Framework Web Chat Control](#using-bot-framework-web-chat-control)** 1. [Render Adaptive Cards using Attachment Middleware](#render-adaptive-cards-using-attachment-middleware) 1. [Send Default Channel Message Tags using Store Middleware](#send-default-channel-message-tags-using-store-middleware) 1. [Data Masking using Store Middleware](#data-masking-using-store-middleware) 1. [Send Typing using Web Chat Props](#send-typing-using-web-chat-props) 1. [Set Upload File Button Visibility](#set-upload-file-button-visibility) 1. [Upload File Validation Middleware using Store Middleware](#upload-file-validation-middleware-using-store-middleware) 1. [Render Multiple Files Upload Middleware using Store Middleware](#render-multiple-files-upload-middleware-using-store-middleware) **[Using Custom Chat Control](#using-custom-chat-control)** 1. [Render Adaptive Cards](#render-adaptive-cards) 1. [Upload File Validation](#upload-file-validation) 1. [Render Messages in Order](#render-messages-in-order) ## Using Bot Framework Web Chat Control ### Render Adaptive Cards using Attachment Middleware ```js import ReactWebChat from 'botframework-webchat'; const supportedAdaptiveCardContentTypes = [ "application/vnd.microsoft.card.adaptive", "application/vnd.microsoft.card.audio", "application/vnd.microsoft.card.hero", "application/vnd.microsoft.card.receipt", "application/vnd.microsoft.card.thumbnail", "application/vnd.microsoft.card.signin", "application/vnd.microsoft.card.oauth", ]; const adaptiveCardKeyValuePairs = `"type": "AdaptiveCard"`; const attachmentMiddleware = () => (next) => (card) => { const { activity: { attachments }, attachment } = card; // No attachment if (!attachments || !attachments.length || !attachment) { return next(card); } let { content, contentType } = attachment || { content: "", contentType: "" }; let { type } = content || { type: "" }; if (supportedAdaptiveCardContentTypes.includes(contentType) || type === 'AdaptiveCard') { // Parse adaptive card content in JSON string format if (content && typeof content === 'string' && content.includes(adaptiveCardKeyValuePairs)) { try { content = JSON.parse(content); type = content.type; card.attachment.content = content; } catch { // Ignore parsing failures to keep chat flowing } } return next(card); } // ...Additional customizations }; // ... return ``` ### Send Default Channel Message Tags using Store Middleware ```js import ReactWebChat, {createStore} from 'botframework-webchat'; const channelIdTag = `ChannelId-lcw`; const customerMessageTag = `FromCustomer`; const sendDefaultMessagingTagsMiddleware = () => (next) => (action) => { const condition = action.type === "DIRECT_LINE/POST_ACTIVITY_PENDING" && action.payload && action.payload.activity && action.payload.activity.channelData; if (condition) { // Add `tags` property if not set if (!action.payload.activity.channelData.tags) { action.payload.activity.channelData.tags = []; } // Add `ChannelId-lcw` tag if not set if (!action.payload.activity.channelData.tags.includes(channelIdTag)) { action.payload.activity.channelData.tags.push(channelIdTag); } // Add `FromCustomer` tag if not set if (!action.payload.activity.channelData.tags.includes(customerMessageTag)) { action.payload.activity.channelData.tags.push(customerMessageTag); } } return next(action); }; const store = createStore( {}, // initial state sendDefaultMessagingTagsMiddleware ); // ... return ``` ### Data Masking using Store Middleware ```js import ReactWebChat, {createStore} from 'botframework-webchat'; // Fetch masking rules const maskingRules = chatSDK.getDataMaskingRules(); const maskingCharacter = '#'; const applyMasking = (text, maskingCharacter) => { // Skip masking on invalid text or masking rules if (!text || !maskingRules || !Object.keys(maskingRules).length) { return text; } for (const maskingRule of Object.values(maskingRules)) { const regex = new RegExp(maskingRule, 'g'); // Masks data let result; while (result = regex.exec(text)) { const replaceStr = result[0].replace(/./g, maskingCharacter); text = text.replace(result[0], replaceStr); } } return text; // Returns masked data } const dataMaskingMiddleware = () => (next) => (action) => { const condition = action.type === "WEB_CHAT/SEND_MESSAGE" && action.payload && action.payload.text && Object.keys(maskingRules).length > 0; if (condition) { action.payload.text = applyMasking(action.payload.text, maskingCharacter); } return next(action); } const store = createStore( {}, // initial state dataMaskingMiddleware ); // ... return ``` ### Send Typing using Web Chat Props ```js import ReactWebChat from 'botframework-webchat'; // ... return ``` ### Set Upload File Button Visibility ```js import ReactWebChat from 'botframework-webchat'; const liveChatConfig = await chatSDK.getLiveChatConfig(); const {LiveWSAndLiveChatEngJoin: liveWSAndLiveChatEngJoin} = liveChatConfig; const {msdyn_enablefileattachmentsforcustomers} = liveWSAndLiveChatEngJoin; const canUploadAttachment = msdyn_enablefileattachmentsforcustomers === "true" || false; const styleOptions = { hideUploadButton: !canUploadAttachment }; // ... return ``` ### Upload File Validation Middleware using Store Middleware ```js import ReactWebChat, {createStore} from 'botframework-webchat'; const liveChatConfig = await chatSDK.getLiveChatConfig(); const {allowedFileExtensions, maxUploadFileSize, LiveWSAndLiveChatEngJoin: liveWSAndLiveChatEngJoin} = liveChatConfig; // maxUploadFileSize in MB const {msdyn_enablefileattachmentsforcustomers} = liveWSAndLiveChatEngJoin; const canUploadAttachment = msdyn_enablefileattachmentsforcustomers === "true" || false; const dispatchAttachmentErrorNotification = (dispatch, message) => { dispatch({ type: "WEB_CHAT/SET_NOTIFICATION", payload: { id: 'attachment', level: 'error', message } }); } const removeAttachment = (attachments, attachmentSizes, index) => { attachments.splice(index, 1); attachmentSizes.splice(index, 1); } const isValidAttachmentFileSize = (fileSizeLimit, attachmentSize) => { return parseInt(fileSizeLimit) * 1024 * 1024 > parseInt(attachmentSize); } const extractFileExtension = (fileName) => { const index = fileName.toLowerCase().lastIndexOf('.'); if (index < 0) { return ''; } return fileName.substring(index); } const isValidAttachmentFileExtension = (supportedFileExtensions, fileExtension) => { return supportedFileExtensions.includes(fileExtension); } const uploadFileValidationMiddleware = ({ dispatch }) => (next) => (action) => { const condition = action.type === "DIRECT_LINE/POST_ACTIVITY" && action.payload && action.payload.activity && action.payload.activity.attachments && action.payload.activity.channelData && action.payload.activity.channelData.attachmentSizes && action.payload.activity.attachments.length === action.payload.activity.channelData.attachmentSizes.length; if (condition) { const {payload: {activity: {attachments, channelData: {attachmentSizes}}}} = action; // Attachment upload capability disabled on admin config if (!canUploadAttachment) { action.payload.activity.attachments = []; action.payload.activity.channelData.attachmentSizes = []; return next(action); } attachments.forEach((attachment: any, i: number) => { const fileExtension = extractFileExtension(attachment.name); const supportedFileExtensions = allowedFileExtensions.toLowerCase().split(','); const isFileEmpty = parseInt(attachmentSizes[i]) === 0; const validFileSize = isValidAttachmentFileSize(maxUploadFileSize, attachmentSizes[i]); const validFileExtension = isValidAttachmentFileExtension(supportedFileExtensions, fileExtension); if (!attachment.name) { const message = `There was an error uploading the file, please try again.`; dispatchAttachmentErrorNotification(dispatch, message); removeAttachment(attachments, attachmentSizes, i); return next(action); } if (!validFileSize && !validFileExtension) { if (!fileExtension) { const message = `File exceeds the allowed limit of ${maxUploadFileSize} MB and please upload the file with an appropriate file extension.`; dispatchAttachmentErrorNotification(dispatch, message); } else { const message = `File exceeds the allowed limit of ${maxUploadFileSize} MB and ${fileExtension} files are not supported.`; dispatchAttachmentErrorNotification(dispatch, message); } removeAttachment(attachments, attachmentSizes, i); return next(action); } if (isFileEmpty) { const message = `This file can't be attached because it's empty. Please try again with a different file.`; dispatchAttachmentErrorNotification(dispatch, message); removeAttachment(attachments, attachmentSizes, i); return next(action); } if (!validFileSize) { const message = `File exceeds the allowed limit of ${maxUploadFileSize} MB`; dispatchAttachmentErrorNotification(dispatch, message); removeAttachment(attachments, attachmentSizes, i); return next(action); } if (!validFileExtension) { if (!fileExtension) { const message = `File upload error. Please upload the file with an appropriate file extension.`; dispatchAttachmentErrorNotification(dispatch, message); } else { const message = `${fileExtension} files are not supported.`; dispatchAttachmentErrorNotification(dispatch, message); } removeAttachment(attachments, attachmentSizes, i); return next(action); } }); } return next(action); } const store = createStore( {}, // initial state uploadFileValidationMiddleware ); // ... return ``` ### Render Multiple Files Upload Middleware using Store Middleware ```js import ReactWebChat, {createStore} from 'botframework-webchat'; const createSendFileAction = (files) => ({ type: "WEB_CHAT/SEND_FILES", payload: { files } }); const renderMultipleFilesUploadMiddleware = ({ dispatch }) => (next) => (action) => { const condition = action.type === "WEB_CHAT/SEND_FILES" && action.payload && action.payload.files && action.payload.files.length > 0 if (condition) { const {payload: {files}} = action; if (files.length === 1) { return next(action); } // Dispatch 'WEB_CHAT/SEND_FILES' action on every file to render all attachments const dispatchAction = createSendFileAction(files.slice(0, files.length - 1)); const nextAction = createSendFileAction([files[files.length - 1]]); dispatch(dispatchAction); return next(nextAction); } return next(action); } const store = createStore( {}, // initial state renderMultipleFilesUploadMiddleware ); // ... return ``` ## Using Custom Chat Control ### Render Adaptive Cards ```js import * as AdaptiveCards from "adaptivecards"; // ... ChatSDK.onNewMessage((message: any) => { const {content} = message; // Adaptive Cards if (content) { if (message.content.includes(`"contentType"`) || message.content.includes(`"suggestedActions"`)) { try { const data = JSON.parse(message.content); if (data.suggestedActions) { // Suggested actions handler // ... } else { if (data.attachments.length > 0) { const adaptiveCard = new AdaptiveCards.AdaptiveCard(); adaptiveCard.parse(data.attachments[0].content); adaptiveCard.onExecuteAction = async (action: any) => { // Adaptive Card event handler const submittedCardResponse = action.data; const content = JSON.stringify({value: submittedCardResponse}); const response = await chatSDK?.sendMessage({content, metadata: { "microsoft.azure.communication.chat.bot.contenttype": "azurebotservice.adaptivecard" }}); }; const renderedCard = adaptiveCard.render(); // Renders as HTML element // Logic to add renderedCard in the DOM } } } catch (error) { console.log('Failed to parse message'); } } } }); ``` ### Upload File Validation ```js const liveChatConfig = await chatSDK.getLiveChatConfig(); const {allowedFileExtensions, maxUploadFileSize} = liveChatConfig; // maxUploadFileSize in MB const isValidAttachmentFileSize = (fileSizeLimit, attachmentSize) => { return parseInt(fileSizeLimit) * 1024 * 1024 > parseInt(attachmentSize); } const extractFileExtension = (fileName) => { const index = fileName.toLowerCase().lastIndexOf('.'); if (index < 0) { return ''; } return fileName.substring(index); } const isValidAttachmentFileExtension = (supportedFileExtensions, fileExtension) => { return supportedFileExtensions.includes(fileExtension); } const fileSelector = document.createElement('input'); fileSelector.setAttribute('type', 'file'); fileSelector.setAttribute('multiple', 'true'); // Allow multiple file inputs (optional) fileSelector.click(); fileSelector.onchange = async (event) => { [...event.target.files].forEach((file) => { const fileExtension = extractFileExtension(file.name); const supportedFileExtensions = allowedFileExtensions.toLowerCase().split(','); const isFileEmpty = parseInt(file.size) === 0; const validFileSize = isValidAttachmentFileSize(maxUploadFileSize, file.size); const validFileExtension = isValidAttachmentFileExtension(supportedFileExtensions, fileExtension); if (!isFileEmpty && validFileSize && validFileExtension) { chatSDK?.uploadFileAttachment(file); } const fileReader = new FileReader(); fileReader.readAsDataURL(file); fileReader.onloadend = () => { // Display Attachment } }); } ``` ### Render Messages in Order > ❗Minimum version of [@microsoft/omnichannel-chat-sdk@1.10.16](https://www.npmjs.com/package/@microsoft/omnichannel-chat-sdk/v/1.10.16) is required ```js class CustomWidgetMessageRenderer { constructor(chatSDK) { this.chatSDK = chatSDK; this.postedMessageIds = new Set(); this.postedOriginalMessageIds = new Set(); this.messages = new Map(); this.subscribers = []; } async initialize(options = {}) { if (options.rehydrate) { setTimeout(async () => { const messages = await this.chatSDK.getMessages(); // Retrieve whole conversation messages messages.forEach(message => { this.postMessage(message); }); }, 500); // Prevent race conditions } await this.chatSDK.onNewMessage((message) => { this.postMessage(message); }); } async sendMessage(content) { const chatMessage = await this.chatSDK.sendMessage({content}); this.postMessage(chatMessage); } getMessages() { // Retrieve ordered messages const messages = [...this.messages.values()]; messages.sort((a, b) => a.id - b.id); // Reorder messages in ascending order return messages; } notifyChatTranscriptUpdate() { const messages = this.getMessages(); this.subscribers.forEach(subscriber => { subscriber(messages); }); } postMessage(newMessage) { const isPostedMessageId = this.postedMessageIds.has(newMessage.id); let isPostedOriginalMessageId = undefined; if (newMessage && newMessage.properties && newMessage.properties.originalMessageId) { // Verify whether the message has originalMessageId isPostedOriginalMessageId = this.postedOriginalMessageIds.has(newMessage.properties.originalMessageId); } if (isPostedMessageId) { const message = this.messages.get(newMessage.id); // Update the message content of queue position message if (newMessage && newMessage.tags && newMessage.tags.includes('queueposition')) { this.messages.set(message.id, {...message, content: newMessage.content}); this.notifyChatTranscriptUpdate(); } } else if (isPostedOriginalMessageId === false) { // Original message id takes precedence over message id this.messages.set(newMessage.properties.originalMessageId, {...newMessage, id: newMessage.properties.originalMessageId}); // Replaces message id with originalMessageId // Update posted message ids this.postedMessageIds.add(newMessage.id); this.postedOriginalMessageIds.add(newMessage.properties.originalMessageId); this.notifyChatTranscriptUpdate(); } else if (!isPostedMessageId) { this.messages.set(newMessage.id, newMessage); this.postedMessageIds.add(newMessage.id); this.notifyChatTranscriptUpdate(); } } onChatTranscriptUpdate(subscriber) { // Subscribe to chat transcript update this.subscribers.push(subscriber); } } const useCustomWidgetMessageRenderer = async (chatSDK, options = {}) => { const renderer = new CustomWidgetMessageRenderer(chatSDK); const initializeOptions = {rehydrate: false}; if (options.rehydrate) { initializeOptions.rehydrate = true; } await renderer.initialize(initializeOptions); return renderer; }; // ... const optionalParams = { liveChatContext }; await chatSDK.startChat(optionalParams); // Set rehydrate option to 'true' only if conversation is rehydrated from liveChatContext or any previously existing conversation (Persistent Chat, Chat Reconnect, etc) const renderer = await useCustomWidgetMessageRenderer(chatSDK, { rehydrate: optionalParams.liveChatContext? true: false }); // Event triggered on new message or when the array of messages have been reordered renderer.onChatTranscriptUpdate((messages) => { // TODO: Add custom implementation to update UI with ordered messages }); renderer.sendMessage("Sample message from customer");