├── .gitignore
├── CODEOWNERS
├── README.md
├── build
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
│ ├── css
│ ├── main.a5c84cc7.css
│ └── main.a5c84cc7.css.map
│ ├── js
│ ├── 453.bcce1d62.chunk.js
│ ├── 453.bcce1d62.chunk.js.map
│ ├── main.d90dfe46.js
│ ├── main.d90dfe46.js.LICENSE.txt
│ └── main.d90dfe46.js.map
│ └── media
│ └── AppLogo.756605568b186bd69dd0.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── AppLogo.png
├── bootstrapMessaging.css
├── bootstrapMessaging.js
├── components
├── conversation.js
├── conversationEntry.css
├── conversationEntry.js
├── messagingBody.css
├── messagingBody.js
├── messagingButton.css
├── messagingButton.js
├── messagingHeader.css
├── messagingHeader.js
├── messagingInputFooter.css
├── messagingInputFooter.js
├── messagingWindow.css
├── messagingWindow.js
├── participantChange.css
├── participantChange.js
├── prechat.css
├── prechat.js
├── textMessage.css
├── textMessage.js
├── typingIndicator.css
└── typingIndicator.js
├── helpers
├── common.js
├── constants.js
├── conversationEntryUtil.js
├── countdownTimer.js
├── eventsource-polyfill.js
├── prechatUtil.js
└── webstorageUtils.js
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
├── services
├── dataProvider.js
├── eventSourceService.js
└── messagingService.js
├── setupTests.js
└── ui-effects
└── draggable.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | # /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing.
2 | #GUSINFO:Embedded Service for Web,Service Community Experience (SCE)
3 | *
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Salesforce Messaging for Web API Sample App
2 |
3 | A repository holding a sample app created using React JS library to demonstrate the Messaging experience using Messaging for In-App and Web Public (aka v2.0) REST APIs.
4 | Make sure to always refer to our [Wiki](https://github.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/wiki/Current-App-Support) for the supported features in the app.
5 |
6 | ## REST API Documentation
7 | [https://developer.salesforce.com/docs/service/messaging-api](https://developer.salesforce.com/docs/service/messaging-api)
8 |
9 | ## Prerequisites
10 | Ensure you have an Embedded Service deployment for Messaging for In-App and Web created of type Custom Client.
11 |
12 | ## Launch Application Remotely
13 | Go to [https://salesforce-async-messaging.github.io/messaging-web-api-sample-app/build/index.html](https://salesforce-async-messaging.github.io/messaging-web-api-sample-app/build/index.html)
14 |
15 | ## Local Development and Testing Setup
16 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
17 | For issues with the sample app, please contact Salesforce Support.
18 |
19 | ### Local Environment requirements
20 | For local app development and testing, make sure you have `npm` or `yarn` installed.
21 |
22 | ### Installation
23 | #### Clone this repo
24 | ```
25 | $ git clone https://github.com/Salesforce-Async-Messaging/messaging-web-api-sample-app.git
26 | ```
27 |
28 | #### Install build dependencies
29 | ```
30 | $ cd messaging-web-api-sample-app
31 | $ npm install
32 | $ npm run build
33 | ```
34 | Builds the app for production to the `build` folder.\
35 | It correctly bundles React in production mode and optimizes the build for the best performance.
36 | The build is minified and the filenames include the hashes.\
37 | Your app is ready to be deployed!
38 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
39 |
40 | #### Launch Application from Local Setup
41 | ```
42 | $ npm start
43 | ```
44 | - Runs the app in the development mode.\
45 | - The page will reload when you make changes.\
46 | - You may also see any lint errors in the console.
47 | - [Local Testing Only] - Disable Strict Mode in the React App by going to `src/index.js` and comment out `React.StrictMode` encapsulating the ``
48 | - After the app is running, open [http://localhost:3000](http://localhost:3000) in your browser to get started.
49 |
50 | ## Test Sample App
51 | - Once the Sample App page is launched either Remotely or via Local Setup, input your Embedded Service deployment details in the form and submit.
52 | - The deployment details can be found under the Code Snippet panel under Embedded Service deployment setup in Salesforce.
53 | - Click on the 'Let's Chat' Button to get started with a new conversation.
54 |
--------------------------------------------------------------------------------
/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.css": "./static/css/main.a5c84cc7.css",
4 | "main.js": "./static/js/main.d90dfe46.js",
5 | "static/js/453.bcce1d62.chunk.js": "./static/js/453.bcce1d62.chunk.js",
6 | "static/media/AppLogo.png": "./static/media/AppLogo.756605568b186bd69dd0.png",
7 | "index.html": "./index.html",
8 | "main.a5c84cc7.css.map": "./static/css/main.a5c84cc7.css.map",
9 | "main.d90dfe46.js.map": "./static/js/main.d90dfe46.js.map",
10 | "453.bcce1d62.chunk.js.map": "./static/js/453.bcce1d62.chunk.js.map"
11 | },
12 | "entrypoints": [
13 | "static/css/main.a5c84cc7.css",
14 | "static/js/main.d90dfe46.js"
15 | ]
16 | }
--------------------------------------------------------------------------------
/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/build/favicon.ico
--------------------------------------------------------------------------------
/build/index.html:
--------------------------------------------------------------------------------
1 |
247 | {shouldShowMessagingButton &&
248 | }
252 | {shouldShowMessagingWindow &&
253 |
254 |
258 |
259 | }
260 | >
261 | );
262 | }
--------------------------------------------------------------------------------
/src/components/conversation.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import * as EventSourcePolyfill from "../helpers/eventsource-polyfill.js";
3 |
4 | // Import children components to plug in and render.
5 | import MessagingHeader from "./messagingHeader";
6 | import MessagingBody from "./messagingBody";
7 | import MessagingInputFooter from "./messagingInputFooter";
8 |
9 | import { setJwt, setLastEventId, storeConversationId, getConversationId, getJwt, clearInMemoryData, setDeploymentConfiguration } from "../services/dataProvider";
10 | import { subscribeToEventSource, closeEventSource } from '../services/eventSourceService';
11 | import { sendTypingIndicator, sendTextMessage, getContinuityJwt, listConversations, listConversationEntries, closeConversation, getUnauthenticatedAccessToken, createConversation } from "../services/messagingService";
12 | import * as ConversationEntryUtil from "../helpers/conversationEntryUtil";
13 | import { CONVERSATION_CONSTANTS, STORAGE_KEYS, CLIENT_CONSTANTS } from "../helpers/constants";
14 | import { setItemInWebStorage, clearWebStorage } from "../helpers/webstorageUtils";
15 | import { util } from "../helpers/common";
16 | import { prechatUtil } from "../helpers/prechatUtil.js";
17 | import Prechat from "./prechat.js";
18 | import CountdownTimer from "../helpers/countdownTimer.js";
19 |
20 | export default function Conversation(props) {
21 | // Initialize a list of conversation entries.
22 | let [conversationEntries, setConversationEntries] = useState([]);
23 | // Initialize the conversation status.
24 | let [conversationStatus, setConversationStatus] = useState(CONVERSATION_CONSTANTS.ConversationStatus.NOT_STARTED_CONVERSATION);
25 | // Tracks whether Pre-Chat form should be shown.
26 | let [showPrechatForm, setShowPrechatForm] = useState(false);
27 | // Tracks the most recent conversation message that was failed to send.
28 | let [failedMessage, setFailedMessage] = useState(undefined);
29 |
30 | // Initialize current participants that are actively typing (except end user).
31 | // Each key is the participant's `senderName` or `role` and each value is a reference to a CountdownTimer object.
32 | let [currentTypingParticipants, setCurrentTypingParticipants] = useState({});
33 | // Initialize whether at least 1 participant (not including end user) is typing.
34 | let [isAnotherParticipantTyping, setIsAnotherParticipantTyping] = useState(false);
35 |
36 | useEffect(() => {
37 | let conversationStatePromise;
38 |
39 | conversationStatePromise = props.isExistingConversation ? handleExistingConversation() : handleNewConversation();
40 | conversationStatePromise
41 | .then(() => {
42 | handleSubscribeToEventSource()
43 | .then(props.uiReady(true)) // Let parent (i.e. MessagingWindow) know the app is UI ready so that the parent can decide to show the actual Messaging window UI.
44 | .catch(() => {
45 | props.showMessagingWindow(false);
46 | })
47 | });
48 |
49 | return () => {
50 | conversationStatePromise
51 | .then(() => {
52 | cleanupMessagingData();
53 | });
54 | };
55 | }, []);
56 |
57 | /**
58 | * Update conversation status state based on the event from a child component i.e. MessagingHeader.
59 | * Updating conversation status state re-renders the current component as well as the child components and the child components can reactively use the updated conversation status to make any changes.
60 | *
61 | * @param {string} status - e.g. CLOSED.
62 | */
63 | function updateConversationStatus(status) {
64 | setConversationStatus(status);
65 | }
66 |
67 | /**
68 | * Handles a new conversation.
69 | *
70 | * 1. Fetch an Unauthenticated Access Token i.e. Messaging JWT.
71 | * 2. Create a new conversation.
72 | * @returns {Promise}
73 | */
74 | function handleNewConversation() {
75 | return handleGetUnauthenticatedJwt()
76 | .then(() => {
77 | if (prechatUtil.shouldDisplayPrechatForm()) {
78 | console.log("Pre-Chat is enabled. Continuing to render a Pre-Chat form.");
79 | setShowPrechatForm(true);
80 | return;
81 | }
82 | console.log("Pre-Chat is not enabled. Continuing to create a new conversation.");
83 | return handleCreateNewConversation()
84 | .then(() => {
85 | console.log(`Completed initializing a new conversation with conversationId: ${getConversationId()}`);
86 | })
87 | .catch(err => {
88 | console.error(`${err}`);
89 | });
90 | })
91 | .catch(err => {
92 | console.error(`${err}`);
93 | });
94 | }
95 |
96 | /**
97 | * Handles an existing conversation.
98 | *
99 | * 1. Fetch a Continuation Access Token i.e. Messaging JWT.
100 | * 2. Lists the available conversations and loads the current (also most-recent) conversation that is OPEN.
101 | * 3. Fetch the entries for the current conversation.
102 | * @returns {Promise}
103 | */
104 | function handleExistingConversation() {
105 | return handleGetContinuityJwt()
106 | .then(() => {
107 | return handleListConversations()
108 | .then(() => {
109 | console.log(`Successfully listed the conversations.`);
110 | handleListConversationEntries()
111 | .then(console.log(`Successfully retrieved entries for the current conversation: ${getConversationId()}`))
112 | .catch(err => {
113 | console.error(`${err}`);
114 | });
115 | })
116 | .catch(err => {
117 | console.error(`${err}`);
118 | });
119 | })
120 | .catch(err => {
121 | console.error(`${err}`);
122 | });
123 | }
124 |
125 | /**
126 | * Handles fetching an Unauthenticated Access Token i.e. Messaging JWT.
127 | *
128 | * 1. If a JWT already exists, simply return.
129 | * 2. Makes a request to Unauthenticated Access Token endpoint.
130 | * 3. Updates the web storage with the latest JWT.
131 | * 4. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful.
132 | * @returns {Promise}
133 | */
134 | function handleGetUnauthenticatedJwt() {
135 | if (getJwt()) {
136 | console.warn("Messaging access token (JWT) already exists in the web storage. Discontinuing to create a new Unauthenticated access token.");
137 | return handleExistingConversation().then(Promise.reject());
138 | }
139 |
140 | return getUnauthenticatedAccessToken()
141 | .then((response) => {
142 | console.log("Successfully fetched an Unauthenticated access token.");
143 | // Parse the response object which includes access-token (JWT), configutation data.
144 | if (typeof response === "object") {
145 | setJwt(response.accessToken);
146 | setItemInWebStorage(STORAGE_KEYS.JWT, response.accessToken);
147 | setLastEventId(response.lastEventId);
148 | setDeploymentConfiguration(response.context && response.context.configuration && response.context.configuration.embeddedServiceConfig);
149 | }
150 | })
151 | .catch((err) => {
152 | console.error(`Something went wrong in fetching an Unauthenticated access token: ${err && err.message ? err.message : err}`);
153 | handleMessagingErrors(err);
154 | cleanupMessagingData();
155 | props.showMessagingWindow(false);
156 | throw new Error("Failed to fetch an Unauthenticated access token.");
157 | });
158 | }
159 |
160 | /**
161 | * Handles creating a new conversation.
162 | *
163 | * 1. If a conversation is already open, simply return.
164 | * 2. Generate a new unique conversation-id and initialize in-memory.
165 | * 3. Makes a request to Create Conversation endpoint.
166 | * 4. Updates the conversation status internally to OPENED, for the associated components to reactively update.
167 | * 5. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful.
168 | * @returns {Promise}
169 | */
170 | function handleCreateNewConversation(routingAttributes) {
171 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION) {
172 | console.warn("Cannot create a new conversation while a conversation is currently open.");
173 | return Promise.reject();
174 | }
175 |
176 | // Initialize a new unique conversation-id in-memory.
177 | storeConversationId(util.generateUUID());
178 | return createConversation(getConversationId(), routingAttributes)
179 | .then(() => {
180 | console.log(`Successfully created a new conversation with conversation-id: ${getConversationId()}`);
181 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION);
182 | props.showMessagingWindow(true);
183 | })
184 | .catch((err) => {
185 | console.error(`Something went wrong in creating a new conversation with conversation-id: ${getConversationId()}: ${err && err.message ? err.message : err}`);
186 | handleMessagingErrors(err);
187 | cleanupMessagingData();
188 | props.showMessagingWindow(false);
189 | throw new Error("Failed to create a new conversation.");
190 | });
191 | }
192 |
193 | /**
194 | * Handles fetching a Continuation Access Token i.e. Messaging JWT.
195 | *
196 | * 1. Makes a request to Continuation Access Token endpoint.
197 | * 2. Updates the web storage with the latest JWT.
198 | * 3. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful.
199 | * @returns {Promise}
200 | */
201 | function handleGetContinuityJwt() {
202 | return getContinuityJwt()
203 | .then((response) => {
204 | setJwt(response.accessToken);
205 | setItemInWebStorage(STORAGE_KEYS.JWT, response.accessToken);
206 | })
207 | .catch((err) => {
208 | console.error(`Something went wrong in fetching a Continuation Access Token: ${err && err.message ? err.message : err}`);
209 | handleMessagingErrors(err);
210 | throw new Error("Failed to fetch a Continuation access token.");
211 | });
212 | }
213 |
214 | /**
215 | * Handles fetching a list of all conversations available. This returns only conversations which are OPEN, unless otherwise specified in the request.
216 | *
217 | * 1. Makes a request to List Conversations endpoint.
218 | * 2. If there are multiple OPEN conversations, loads the conversation with the most-recent start time.
219 | * 3. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful.
220 | * @returns {Promise}
221 | */
222 | function handleListConversations() {
223 | return listConversations()
224 | .then((response) => {
225 | if (response && response.openConversationsFound > 0 && response.conversations.length) {
226 | const openConversations = response.conversations;
227 | if (openConversations.length > 1) {
228 | console.warn(`Expected the user to be participating in 1 open conversation but instead found ${openConversations.length}. Loading the conversation with latest startTimestamp.`);
229 | openConversations.sort((conversationA, conversationB) => conversationB.startTimestamp - conversationA.startTimestamp);
230 | }
231 | // Update conversation-id with the one from service.
232 | storeConversationId(openConversations[0].conversationId);
233 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION);
234 | props.showMessagingWindow(true);
235 | } else {
236 | // No open conversations found.
237 | cleanupMessagingData();
238 | props.showMessagingWindow(false);
239 | }
240 | })
241 | .catch((err) => {
242 | console.error(`Something went wrong in fetching a list of conversations: ${err && err.message ? err.message : err}`);
243 | handleMessagingErrors(err);
244 | throw new Error("Failed to list the conversations.");
245 | });
246 | }
247 |
248 | /**
249 | * Handles fetching a list of all conversation entries for the current conversation.
250 | *
251 | * 1. Makes a request to List Conversation Entries endpoint.
252 | * 2. Renders the conversation entries based on their Entry Type.
253 | * @returns {Promise}
254 | */
255 | function handleListConversationEntries() {
256 | return listConversationEntries(getConversationId())
257 | .then((response) => {
258 | if (Array.isArray(response)) {
259 | response.reverse().forEach(entry => {
260 | const conversationEntry = generateConversationEntryForCurrentConversation(entry);
261 | if (!conversationEntry) {
262 | return;
263 | }
264 |
265 | switch (conversationEntry.entryType) {
266 | case CONVERSATION_CONSTANTS.EntryTypes.CONVERSATION_MESSAGE:
267 | conversationEntry.isEndUserMessage = ConversationEntryUtil.isMessageFromEndUser(conversationEntry);
268 | addConversationEntry(conversationEntry);
269 | break;
270 | case CONVERSATION_CONSTANTS.EntryTypes.PARTICIPANT_CHANGED:
271 | case CONVERSATION_CONSTANTS.EntryTypes.ROUTING_RESULT:
272 | addConversationEntry(conversationEntry);
273 | break;
274 | default:
275 | console.log(`Unrecognized conversation entry type: ${conversationEntry.entryType}.`);
276 | }
277 | });
278 | } else {
279 | console.error(`Expecting a response of type Array from listConversationEntries but instead received: ${response}`);
280 | }
281 | })
282 | .catch((err) => {
283 | console.error(`Something went wrong while processing entries from listConversationEntries response: ${err && err.message ? err.message : err}`);
284 | handleMessagingErrors(err);
285 | throw new Error("Failed to list the conversation entries for the current conversation.");
286 | });
287 | }
288 |
289 | /**
290 | * Handles establishing a connection to the EventSource i.e. SSE.
291 | * Selectively listens to the supported events in the app by adding the corresponding event listeners.
292 | * Note: Update the list of events/event-listeners to add/remove support for the available events. Refer https://developer.salesforce.com/docs/service/messaging-api/references/about/server-sent-events-structure.html
293 | * @returns {Promise}
294 | */
295 | function handleSubscribeToEventSource() {
296 | return subscribeToEventSource({
297 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_MESSAGE]: handleConversationMessageServerSentEvent,
298 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_ROUTING_RESULT]: handleRoutingResultServerSentEvent,
299 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_PARTICIPANT_CHANGED]: handleParticipantChangedServerSentEvent,
300 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_TYPING_STARTED_INDICATOR]: handleTypingStartedIndicatorServerSentEvent,
301 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_TYPING_STOPPED_INDICATOR]: handleTypingStoppedIndicatorServerSentEvent,
302 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_DELIVERY_ACKNOWLEDGEMENT]: handleConversationDeliveryAcknowledgementServerSentEvent,
303 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_READ_ACKNOWLEDGEMENT]: handleConversationReadAcknowledgementServerSentEvent,
304 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_CLOSE_CONVERSATION]: handleCloseConversationServerSentEvent
305 | })
306 | .then(() => {
307 | console.log("Subscribed to the Event Source (SSE).");
308 | })
309 | .catch((err) => {
310 | handleMessagingErrors(err);
311 | throw new Error(err);
312 | });
313 | }
314 |
315 | /**
316 | * Generate a Conversation Entry object from the server sent event.
317 | *
318 | * 1. Create a Conversation Entry object from the parsed event data.
319 | * 2. Return the Conversation Entry if the conversationEntry is for the current conversation and undefined, otherwise.
320 | * @param {object} event - Event data payload from server-sent event.
321 | * @returns {object|undefined}
322 | */
323 | function generateConversationEntryForCurrentConversation(parsedEventData) {
324 | const conversationEntry = ConversationEntryUtil.createConversationEntry(parsedEventData);
325 |
326 | // Handle server sent events only for the current conversation
327 | if (parsedEventData.conversationId === getConversationId()) {
328 | return conversationEntry;
329 | }
330 | console.log(`Current conversation-id: ${getConversationId()} does not match the conversation-id in server sent event: ${parsedEventData.conversationId}. Ignoring the event.`);
331 | return undefined;
332 | }
333 |
334 | /**
335 | * Adds a Conversation Entry object to the list of conversation entries. Updates the state of the list of conversation entries for the component(s) to be updated in-turn, reactively.
336 | * @param {object} conversationEntry - entry object for the current conversation.
337 | */
338 | function addConversationEntry(conversationEntry) {
339 | conversationEntries.push(conversationEntry);
340 | setConversationEntries([...conversationEntries]);
341 | }
342 |
343 | /**
344 | * Handle a CONVERSATION_MESSAGE server-sent event.
345 | *
346 | * This includes:
347 | * 1. Parse, populate, and create ConversationEntry object based on its entry type
348 | * NOTE: Skip processing CONVERSATION_MESSAGE if the newly created ConversationEntry is undefined or invalid or not from the current conversation.
349 | * 2. Updates in-memory list of conversation entries and the updated list gets reactively passed on to MessagingBody.
350 | * @param {object} event - Event data payload from server-sent event.
351 | */
352 | function handleConversationMessageServerSentEvent(event) {
353 | try {
354 | console.log(`Successfully handling conversation message server sent event.`);
355 | // Update in-memory to the latest lastEventId
356 | if (event && event.lastEventId) {
357 | setLastEventId(event.lastEventId);
358 | }
359 |
360 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
361 | const conversationEntry = generateConversationEntryForCurrentConversation(parsedEventData);
362 | if (!conversationEntry) {
363 | return;
364 | }
365 |
366 | if (ConversationEntryUtil.isMessageFromEndUser(conversationEntry)) {
367 | conversationEntry.isEndUserMessage = true;
368 |
369 | // Since message is echoed back by the server, mark the conversation entry as sent.
370 | conversationEntry.isSent = true;
371 |
372 | console.log(`End user successfully sent a message.`);
373 | } else {
374 | conversationEntry.isEndUserMessage = false;
375 | console.log(`Successfully received a message from ${conversationEntry.actorType}`);
376 | }
377 |
378 | addConversationEntry(conversationEntry);
379 | } catch(err) {
380 | console.error(`Something went wrong in handling conversation message server sent event: ${err}`);
381 | }
382 | }
383 |
384 | /**
385 | * Handle a ROUTING_RESULT server-sent event.
386 | *
387 | * This includes:
388 | * 1. Parse, populate, and create ConversationEntry object based on its entry type.
389 | * NOTE: Skip processing ROUTING_RESULT if the newly created ConversationEntry is undefined or invalid or not from the current conversation.
390 | * 2. Updates in-memory list of conversation entries and the updated list gets reactively passed on to MessagingBody.
391 | *
392 | * NOTE: Update the chat client based on the latest routing result. E.g. if the routing type is transfer, set an internal flag like `isTransferring` to 'true' and use that to show a transferring indicator in the ui.
393 | * @param {object} event - Event data payload from server-sent event.
394 | */
395 | function handleRoutingResultServerSentEvent(event) {
396 | try {
397 | console.log(`Successfully handling routing result server sent event.`);
398 | // Update in-memory to the latest lastEventId
399 | if (event && event.lastEventId) {
400 | setLastEventId(event.lastEventId);
401 | }
402 |
403 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
404 | const conversationEntry = generateConversationEntryForCurrentConversation(parsedEventData);
405 | if (!conversationEntry) {
406 | return;
407 | }
408 |
409 | if (conversationEntry.messageType === CONVERSATION_CONSTANTS.RoutingTypes.INITIAL) {
410 | // Render reasonForNotRouting when initial routing fails.
411 | switch (conversationEntry.content.failureType) {
412 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.NO_ERROR:
413 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.SUBMISSION_ERROR:
414 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.ROUTING_ERROR:
415 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.UNKNOWN_ERROR:
416 | addConversationEntry(conversationEntry);
417 | break;
418 | default:
419 | console.error(`Unrecognized initial routing failure type: ${conversationEntry.content.failureType}`);
420 | }
421 | // Handle when a conversation is being transferred.
422 | } else if (conversationEntry.messageType === CONVERSATION_CONSTANTS.RoutingTypes.TRANSFER) {
423 | switch (conversationEntry.content.failureType) {
424 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.NO_ERROR:
425 | // Render transfer timestamp when transfer is requested successfully.
426 | // TODO: Add a transfer state ui update.
427 | addConversationEntry(conversationEntry);
428 | break;
429 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.SUBMISSION_ERROR:
430 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.ROUTING_ERROR:
431 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.UNKNOWN_ERROR:
432 | break;
433 | default:
434 | console.error(`Unrecognized transfer routing failure type: ${conversationEntry.content.failureType}`);
435 | }
436 | } else {
437 | console.error(`Unrecognized routing type: ${conversationEntry.messageType}`);
438 | }
439 | } catch (err) {
440 | console.error(`Something went wrong in handling routing result server sent event: ${err}`);
441 | }
442 | }
443 |
444 | /**
445 | * Handle a PARTICIPANT_CHANGED server-sent event.
446 | *
447 | * This includes:
448 | * 1. Parse, populate, and create ConversationEntry object based on its entry type.
449 | * NOTE: Skip processing PARTICIPANT_CHANGED if the newly created ConversationEntry is undefined or invalid or not from the current conversation.
450 | * 2. Updates in-memory list of conversation entries and the updated list gets reactively passed on to MessagingBody.
451 | * @param {object} event - Event data payload from server-sent event.
452 | */
453 | function handleParticipantChangedServerSentEvent(event) {
454 | try {
455 | console.log(`Successfully handling participant changed server sent event.`);
456 | // Update in-memory to the latest lastEventId
457 | if (event && event.lastEventId) {
458 | setLastEventId(event.lastEventId);
459 | }
460 |
461 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
462 | const conversationEntry = generateConversationEntryForCurrentConversation(parsedEventData);
463 | if (!conversationEntry) {
464 | return;
465 | }
466 | addConversationEntry(conversationEntry);
467 | } catch (err) {
468 | console.error(`Something went wrong in handling participant changed server sent event: ${err}`);
469 | }
470 | }
471 |
472 | /**
473 | * Handle a TYPING_STARTED_INDICATOR server-sent event.
474 | * @param {object} event - Event data payload from server-sent event.
475 | */
476 | function handleTypingStartedIndicatorServerSentEvent(event) {
477 | try {
478 | // Update in-memory to the latest lastEventId
479 | if (event && event.lastEventId) {
480 | setLastEventId(event.lastEventId);
481 | }
482 |
483 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
484 | // Handle typing indicators only for the current conversation
485 | if (getConversationId() === parsedEventData.conversationId) {
486 | const senderName = ConversationEntryUtil.getSenderDisplayName(parsedEventData) || ConversationEntryUtil.getSenderRole(parsedEventData);
487 | const typingParticipantTimer = currentTypingParticipants[senderName] && currentTypingParticipants[senderName].countdownTimer;
488 |
489 | // If we have received typing indicator from this sender within the past 5 seconds, reset the timer
490 | // Otherwise, start a new timer
491 | if (ConversationEntryUtil.getSenderRole(parsedEventData) !== CONVERSATION_CONSTANTS.ParticipantRoles.ENDUSER) {
492 | console.log(`Successfully handling typing started indicator server sent event.`);
493 |
494 | if (typingParticipantTimer) {
495 | typingParticipantTimer.reset(Date.now());
496 | } else {
497 | currentTypingParticipants[senderName] = {
498 | countdownTimer: new CountdownTimer(() => {
499 | delete currentTypingParticipants[senderName];
500 |
501 | if (!Object.keys(currentTypingParticipants).length) {
502 | setIsAnotherParticipantTyping(false);
503 | }
504 | }, CLIENT_CONSTANTS.TYPING_INDICATOR_DISPLAY_TIMEOUT, Date.now()),
505 | role: parsedEventData.conversationEntry.sender.role
506 | };
507 |
508 | currentTypingParticipants[senderName].countdownTimer.start();
509 | }
510 |
511 | setIsAnotherParticipantTyping(true);
512 | }
513 | }
514 | } catch (err) {
515 | console.error(`Something went wrong in handling typing started indicator server sent event: ${err}`);
516 | }
517 | }
518 |
519 | /**
520 | * Handle a TYPING_STOPPED_INDICATOR server-sent event.
521 | * @param {object} event - Event data payload from server-sent event.
522 | */
523 | function handleTypingStoppedIndicatorServerSentEvent(event) {
524 | try {
525 | // Update in-memory to the latest lastEventId
526 | if (event && event.lastEventId) {
527 | setLastEventId(event.lastEventId);
528 | }
529 |
530 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
531 | // Handle typing indicators only for the current conversation
532 | if (getConversationId() === parsedEventData.conversationId) {
533 | const senderName = ConversationEntryUtil.getSenderDisplayName(parsedEventData) || ConversationEntryUtil.getSenderRole(parsedEventData);
534 |
535 | delete currentTypingParticipants[senderName];
536 |
537 | if (!Object.keys(currentTypingParticipants).length) {
538 | setIsAnotherParticipantTyping(false);
539 | }
540 | }
541 | } catch (err) {
542 | console.error(`Something went wrong in handling typing stopped indicator server sent event: ${err}`);
543 | }
544 | }
545 |
546 | /**
547 | * Handle a server-sent event CONVERSATION_DELIVERY_ACKNOWLEDGEMENT:
548 | * - Parse server-sent event and update a conversation entry (if it exists) with data from the event.
549 | * - Store delivery acknowledgement timestamp on conversation entry to show receipt timestamp.
550 | * @param {object} event - Event data payload from server-sent event.
551 | */
552 | function handleConversationDeliveryAcknowledgementServerSentEvent(event) {
553 | try {
554 | console.log(`Successfully handling conversation delivery acknowledgement server sent event.`);
555 | // Update in-memory to the latest lastEventId
556 | if (event && event.lastEventId) {
557 | setLastEventId(event.lastEventId);
558 | }
559 |
560 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
561 | // Handle delivery acknowledgements only for the current conversation
562 | if (getConversationId() === parsedEventData.conversationId) {
563 | if (parsedEventData.conversationEntry && parsedEventData.conversationEntry.relatedRecords.length && parsedEventData.conversationEntry.entryPayload.length) {
564 | // Store acknowledgement timestamp and identifier of message that was delivered.
565 | const deliveredMessageId = parsedEventData.conversationEntry.relatedRecords[0];
566 | const deliveryAcknowledgementTimestamp = JSON.parse(parsedEventData.conversationEntry.entryPayload).acknowledgementTimestamp;
567 |
568 | // Make deep copy of conversation entries to find matching messageId.
569 | util.createDeepCopy(conversationEntries).filter((conversationEntry) => {
570 | if (conversationEntry.messageId === deliveredMessageId) {
571 | conversationEntry.isDelivered = true;
572 | conversationEntry.deliveryAcknowledgementTimestamp = deliveryAcknowledgementTimestamp;
573 | }
574 | });
575 | }
576 | }
577 | } catch (err) {
578 | console.error(`Something went wrong in handling conversation delivery acknowledgement server sent event: ${err}`);
579 | }
580 | }
581 |
582 | /**
583 | * Handle a server-sent event CONVERSATION_READ_ACKNOWLEDGEMENT:
584 | * - Parse server-sent event and update a conversation entry (if it exists) with data from the event.
585 | * - Store read acknowledgement timestamp on conversation entry to show receipt timestamp.
586 | * @param {object} event - Event data payload from server-sent event.
587 | */
588 | function handleConversationReadAcknowledgementServerSentEvent(event) {
589 | try {
590 | console.log(`Successfully handling conversation read acknowledgement server sent event.`);
591 | // Update in-memory to the latest lastEventId
592 | if (event && event.lastEventId) {
593 | setLastEventId(event.lastEventId);
594 | }
595 |
596 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
597 | // Handle read acknowledgements only for the current conversation
598 | if (getConversationId() === parsedEventData.conversationId) {
599 | if (parsedEventData.conversationEntry && parsedEventData.conversationEntry.relatedRecords.length && parsedEventData.conversationEntry.entryPayload.length) {
600 | // Store acknowledgement timestamp and identifier of message that was read.
601 | const readMessageId = parsedEventData.conversationEntry.relatedRecords[0];
602 | const readAcknowledgementTimestamp = JSON.parse(parsedEventData.conversationEntry.entryPayload).acknowledgementTimestamp;
603 |
604 | // Make deep copy of conversation entries to find matching messageId.
605 | util.createDeepCopy(conversationEntries).filter((conversationEntry) => {
606 | if (conversationEntry.messageId === readMessageId) {
607 | conversationEntry.isRead = true;
608 | conversationEntry.readAcknowledgementTimestamp = readAcknowledgementTimestamp;
609 | }
610 | });
611 | }
612 | }
613 | } catch (err) {
614 | console.error(`Something went wrong in handling conversation read acknowledgement server sent event: ${err}`);
615 | }
616 | }
617 |
618 | /**
619 | * Handle a CONVERSATION_CLOSED server-sent event.
620 | *
621 | * @param {object} event - Event data payload from server-sent event.
622 | */
623 | function handleCloseConversationServerSentEvent(event) {
624 | try {
625 | console.log(`Successfully handling close conversation server sent event.`);
626 | // Update in-memory to the latest lastEventId
627 | if (event && event.lastEventId) {
628 | setLastEventId(event.lastEventId);
629 | }
630 |
631 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event);
632 |
633 | // Do not render conversation ended text if the conversation entry is not for the current conversation.
634 | if (getConversationId() === parsedEventData.conversationId) {
635 | // Update state to conversation closed status.
636 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION);
637 | }
638 | } catch (err) {
639 | console.error(`Something went wrong while handling conversation closed server sent event in conversation ${getConversationId()}: ${err}`);
640 | }
641 | }
642 |
643 | /**
644 | * Handle sending a static text message in a conversation.
645 | * @param {string} conversationId - Identifier of the conversation.
646 | * @param {string} value - Actual text of the message.
647 | * @param {string} messageId - Unique identifier of the message.
648 | * @param {string} inReplyToMessageId - Identifier of another message where this message is being replied to.
649 | * @param {boolean} isNewMessagingSession - Whether it is a new messaging session.
650 | * @param {object} routingAttributes - Pre-Chat fields and its values, if configured.
651 | * @param {object} language - language code.
652 | *
653 | * @returns {Promise}
654 | */
655 | function handleSendTextMessage(conversationId, value, messageId, inReplyToMessageId, isNewMessagingSession, routingAttributes, language) {
656 | return sendTextMessage(conversationId, value, messageId, inReplyToMessageId, isNewMessagingSession, routingAttributes, language)
657 | .catch((err) => {
658 | console.error(`Something went wrong while sending a message to conversation ${conversationId}: ${err}`);
659 | setFailedMessage(Object.assign({}, {messageId, value, inReplyToMessageId, isNewMessagingSession, routingAttributes, language}));
660 | handleMessagingErrors(err);
661 | });
662 | }
663 |
664 | /**
665 | * Close messaging window handler for the event from a child component i.e. MessagingHeader.
666 | * When such event is received, invoke the parent's handler to close the messaging window if the conversation status is closed or not yet started.
667 | */
668 | function endConversation() {
669 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION) {
670 | // End the conversation if it is currently opened.
671 | return closeConversation(getConversationId())
672 | .then(() => {
673 | console.log(`Successfully closed the conversation with conversation-id: ${getConversationId()}`);
674 | })
675 | .catch((err) => {
676 | console.error(`Something went wrong in closing the conversation with conversation-id ${getConversationId()}: ${err}`);
677 | })
678 | .finally(() => {
679 | cleanupMessagingData();
680 | });
681 | }
682 | }
683 |
684 | /**
685 | * Close messaging window handler for the event from a child component i.e. MessagingHeader.
686 | * When such event is received, invoke the parent's handler to close the messaging window if the conversation status is closed or not yet started.
687 | */
688 | function closeMessagingWindow() {
689 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION || conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.NOT_STARTED_CONVERSATION) {
690 | props.showMessagingWindow(false);
691 | }
692 | }
693 |
694 | /**
695 | * Performs a cleanup in the app.
696 | * 1. Closes the EventSource connection.
697 | * 2. Clears the web storage.
698 | * 3. Clears the in-memory messaging data.
699 | * 4. Update the internal conversation status to CLOSED.
700 | */
701 | function cleanupMessagingData() {
702 | closeEventSource()
703 | .then(console.log("Closed the Event Source (SSE)."))
704 | .catch((err) => {
705 | console.error(`Something went wrong in closing the Event Source (SSE): ${err}`);
706 | });
707 |
708 | clearWebStorage();
709 | clearInMemoryData();
710 |
711 | // Update state to conversation closed status.
712 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION);
713 | }
714 |
715 | /**
716 | * Handles the errors from messaging endpoint requests.
717 | * If a request is failed due to an Unauthorized error (i.e. 401), peforms a cleanup and resets the app and console logs otherwise.
718 | */
719 | function handleMessagingErrors(err) {
720 | if (typeof err === "object") {
721 | if (err.status) {
722 | switch (err.status) {
723 | case 401:
724 | console.error(`Unauthenticated request: ${err.message}`);
725 | cleanupMessagingData();
726 | props.showMessagingWindow(false);
727 | break;
728 | case 400:
729 | console.error(`Invalid request parameters. Please check your data before retrying: ${err.message}`);
730 | break;
731 | case 404:
732 | console.error(`Resource not found. Please check your data before retrying: ${err.message}`);
733 | break;
734 | case 429:
735 | console.warn(`Too many requests issued from the app. Try again in sometime: ${err.message}`);
736 | break;
737 | /**
738 | * HTTP error code returned by the API(s) when a message is sent and failed because no messaging session exists. This error indicates client
739 | * to surface the Pre-Chat form if configured to show for every new messaging session and then retry the failed message with routingAttributes.
740 | */
741 | case 417:
742 | if (prechatUtil.shouldDisplayPrechatForm() && prechatUtil.shouldDisplayPrechatEveryMessagingSession()) {
743 | console.log("Pre-Chat configured to show for every new messaging session. Continuing to display the Pre-Chat form.");
744 | setShowPrechatForm(true);
745 | }
746 | case 500:
747 | console.error(`Something went wrong in the request, please try again: ${err.message}`);
748 | break;
749 | default:
750 | console.error(`Unhandled/Unknown http error: ${err}`);
751 | cleanupMessagingData();
752 | props.showMessagingWindow(false);
753 | }
754 | return;
755 | }
756 | console.error(`Something went wrong: ${err && err.message ? err.message : err}`);
757 | }
758 | return;
759 | }
760 |
761 | /**
762 | * Handles submitting a Pre-Chat form to either start a new conversation or a new messaging session.
763 | * @param {object} prechatData - Object containing key-value pairs of Pre-Chat form fields and their corresponding values.
764 | */
765 | function handlePrechatSubmit(prechatData) {
766 | let prechatSubmitPromise;
767 | console.log(`Pre-Chat fields values on submit: ${prechatData}.`);
768 |
769 | // If there is a failed message being tracked while submitting Pre-Chat form, consider it is an existing conversation but a new messaging session. Resend the failed message with the routing attributes to begin a new messaging session.
770 | if (failedMessage) {
771 | prechatSubmitPromise = handleSendTextMessage(getConversationId(), failedMessage.value, failedMessage.messageId, failedMessage.inReplyToMessageId, true, prechatData, failedMessage.language);
772 | } else {
773 | // If there is no failed message being tracked while submitting Pre-Chat form, create a new conversation.
774 | prechatSubmitPromise = handleCreateNewConversation(prechatData);
775 | }
776 | prechatSubmitPromise
777 | .then(() => {
778 | setShowPrechatForm(false);
779 | });
780 | }
781 |
782 | return (
783 | <>
784 |
788 | {!showPrechatForm &&
789 | <>
790 |
795 |
799 | >
800 | }
801 | {
802 | showPrechatForm &&
803 |
804 | }
805 | >
806 | );
807 | }
--------------------------------------------------------------------------------
/src/components/conversationEntry.css:
--------------------------------------------------------------------------------
1 | .conversationEntryContainer {
2 | position: relative;
3 | overflow: auto;
4 | margin-top: 10px;
5 | margin-bottom: 10px;
6 | }
--------------------------------------------------------------------------------
/src/components/conversationEntry.js:
--------------------------------------------------------------------------------
1 | import "./conversationEntry.css";
2 | import * as ConversationEntryUtil from "../helpers/conversationEntryUtil";
3 |
4 | // Import children components to plug in and render.
5 | import TextMessage from "./textMessage";
6 | import ParticipantChange from "./participantChange";
7 |
8 | export default function ConversationEntry({conversationEntry}) {
9 |
10 | return (
11 | <>
12 |
13 | {/* Render component for a conversation entry of type Text Message. */}
14 | {ConversationEntryUtil.isTextMessage(conversationEntry) && }
15 | {/* Render component for a conversation entry of type Participant Change. */}
16 | {ConversationEntryUtil.isParticipantChangeEvent(conversationEntry) && }
17 |