@meetelise/chat 1.21.0 → 1.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.github/pull_request_template.md +61 -0
  2. package/.idea/codeStyles/Project.xml +57 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/.idea/workspace.xml +67 -0
  7. package/README.md +29 -14
  8. package/declarations.d.ts +12 -0
  9. package/package.json +5 -1
  10. package/public/demo/index.html +62 -4
  11. package/public/demo/secret.html +63 -0
  12. package/public/dist/index.js +3184 -1105
  13. package/public/dist/index.js.LICENSE.txt +19 -9
  14. package/public/index.html +6 -4
  15. package/src/MEChat.ts +207 -52
  16. package/src/MyPubnub.ts +657 -0
  17. package/src/WebComponent/LeadSourceClient.ts +300 -0
  18. package/src/WebComponent/Scheduler/date-picker.ts +1 -1
  19. package/src/WebComponent/Scheduler/time-picker.ts +86 -76
  20. package/src/WebComponent/Scheduler/tour-scheduler.ts +694 -764
  21. package/src/WebComponent/Scheduler/tour-type-option.ts +17 -3
  22. package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +418 -0
  23. package/src/WebComponent/actions/InputStyles.ts +32 -10
  24. package/src/WebComponent/actions/action-confirm-button.ts +16 -11
  25. package/src/WebComponent/actions/call-us-window.ts +341 -58
  26. package/src/WebComponent/actions/details-window.ts +30 -16
  27. package/src/WebComponent/actions/email-us-window.ts +89 -58
  28. package/src/WebComponent/actions/formatPhoneNumber.ts +15 -1
  29. package/src/WebComponent/actions/minimize-expand-button.ts +92 -0
  30. package/src/WebComponent/health-chat.ts +267 -0
  31. package/src/WebComponent/healthcare/healthcare-launcher-styles.ts +34 -0
  32. package/src/WebComponent/healthcare/healthcare-launcher.ts +100 -0
  33. package/src/WebComponent/healthchat-styles.ts +119 -0
  34. package/src/WebComponent/index.ts +1 -1
  35. package/src/WebComponent/launcher/Launcher.ts +919 -0
  36. package/src/WebComponent/{launcherStyles.ts → launcher/launcherStyles.ts} +172 -29
  37. package/src/WebComponent/launcher/mobile-launcher.ts +127 -0
  38. package/src/WebComponent/launcher/typeEmojiStyles.ts +161 -0
  39. package/src/WebComponent/launcher/typeMiniStyles.ts +60 -0
  40. package/src/WebComponent/launcher/typeMobileStyles.ts +50 -0
  41. package/src/WebComponent/leasing-chat-styles.ts +114 -0
  42. package/src/WebComponent/me-chat.ts +964 -351
  43. package/src/WebComponent/me-select.ts +48 -21
  44. package/src/WebComponent/mini-loader.ts +28 -0
  45. package/src/WebComponent/pubnub-chat-styles.ts +192 -0
  46. package/src/WebComponent/pubnub-chat.ts +707 -0
  47. package/src/WebComponent/pubnub-media.ts +208 -0
  48. package/src/WebComponent/pubnub-message-styles.ts +54 -0
  49. package/src/WebComponent/pubnub-message.ts +421 -0
  50. package/src/analytics.ts +114 -14
  51. package/src/assetUrls.ts +2 -0
  52. package/src/disclaimers.ts +56 -0
  53. package/src/fetchBuildingABTestType.ts +4 -0
  54. package/src/fetchBuildingInfo.ts +25 -17
  55. package/src/fetchFeatureFlag.ts +147 -0
  56. package/src/fetchLeadSources.ts +67 -1
  57. package/src/fetchPhoneNumberFromSource.ts +31 -0
  58. package/src/fetchWebchatPreferences.ts +55 -0
  59. package/src/getAvailabilities.ts +7 -3
  60. package/src/getBuildingPhoneNumber.ts +26 -0
  61. package/src/getShouldAllowScheduling.ts +16 -0
  62. package/src/getTimezoneString.ts +39 -0
  63. package/src/gtm.ts +17 -0
  64. package/src/handleChatId.ts +101 -0
  65. package/src/insertDNIIntoWebsite.ts +136 -0
  66. package/src/insertLeadSourceIntoSchedulerLinks.ts +50 -0
  67. package/src/postLeadSources.ts +39 -35
  68. package/src/svgIcons.ts +62 -53
  69. package/src/themes.ts +47 -121
  70. package/src/utils.ts +88 -1
  71. package/src/WebComponent/Launcher.ts +0 -559
  72. package/src/WebComponent/actions/text-us-window.ts +0 -279
  73. package/src/chatID.ts +0 -64
  74. package/src/createConversation.ts +0 -57
  75. package/src/fetchCurrentParsedLeadSource.ts +0 -24
  76. package/src/getRegisteredPhoneNumbers.ts +0 -56
@@ -0,0 +1,657 @@
1
+ import { AxiosError } from "axios";
2
+ import Pubnub, { ListenerParameters, MessageEvent } from "pubnub";
3
+
4
+ import axios from "axios";
5
+ import { Building } from "./fetchBuildingInfo";
6
+ import { LogType, sendLoggingEvent } from "./analytics";
7
+ import { ChatStorageKey, createChatStorageKey } from "./handleChatId";
8
+ import LeadSourceClient from "./WebComponent/LeadSourceClient";
9
+ import { pushGtmEvent } from "./gtm";
10
+ import { isContainingEmail } from "./utils";
11
+
12
+ interface TokenResponse {
13
+ auth: {
14
+ result: {
15
+ token: string;
16
+ };
17
+ };
18
+ keys: {
19
+ subscribe_key: string;
20
+ publish_key: string;
21
+ };
22
+ }
23
+
24
+ // This is what we expect from our BE
25
+ enum MessageType {
26
+ noReply = "no-reply",
27
+ text = "text",
28
+ media = "media",
29
+ }
30
+
31
+ interface RawPubnubMessage {
32
+ channel: string;
33
+ message: {
34
+ // all the possible options from our BE
35
+ text: string;
36
+ customType: string;
37
+ messageType?: MessageType;
38
+ is_streaming?: boolean;
39
+ is_done?: boolean;
40
+ order?: number;
41
+ stream_id?: string;
42
+ media?: ChatMediaFile[];
43
+ };
44
+ publisher: string;
45
+ subscription: string;
46
+ timetoken: number;
47
+ }
48
+
49
+ export interface SimpleTextChatMessage {
50
+ timestamp: number;
51
+ message: string;
52
+ isLeadMessage: boolean;
53
+ chunks: {
54
+ text: string;
55
+ order: number;
56
+ isDone: boolean;
57
+ }[];
58
+ type: SimpleMessageTypes.text;
59
+ }
60
+
61
+ interface ChatMediaFile {
62
+ title: string | null;
63
+ description: string | null;
64
+ url: string | null;
65
+ }
66
+ export interface SimpleMediaChatMessage {
67
+ timestamp: number;
68
+ media: ChatMediaFile[];
69
+ isLeadMessage: boolean;
70
+ type: SimpleMessageTypes.media;
71
+ }
72
+
73
+ export type SimpleChatMessage = SimpleTextChatMessage | SimpleMediaChatMessage;
74
+
75
+ export const isSimpleTextChatMessage = (
76
+ message: SimpleChatMessage
77
+ ): message is SimpleTextChatMessage => {
78
+ return message.type === SimpleMessageTypes.text;
79
+ };
80
+
81
+ export const isSimpleMediaChatMessage = (
82
+ message: SimpleChatMessage
83
+ ): message is SimpleMediaChatMessage => {
84
+ return message.type === SimpleMessageTypes.media;
85
+ };
86
+
87
+ export enum SimpleMessageTypes {
88
+ text = "text",
89
+ media = "media",
90
+ }
91
+
92
+ class MyPubnub {
93
+ private apiHost = "https://app.meetelise.com";
94
+
95
+ private leadSourceClient: LeadSourceClient | null = null;
96
+ private building: Building | null = null;
97
+ private buildingSlug: string;
98
+ private orgSlug: string;
99
+
100
+ private eliseResponseTimeout: NodeJS.Timeout | null = null;
101
+
102
+ pubnub: Pubnub | null = null;
103
+ leadUserId = "";
104
+ channel = "";
105
+ leadSource: string | null = null;
106
+
107
+ isCurrentlyStreamingMessage = false;
108
+
109
+ chatListener:
110
+ | ((res: { messages: SimpleChatMessage[]; isLoading: boolean }) => void)
111
+ | null = null;
112
+
113
+ rawPubnubMessages: RawPubnubMessage[] = [];
114
+ simpleChatMessages: SimpleChatMessage[] = [];
115
+
116
+ listenerParams: ListenerParameters = {
117
+ message: (messageEvent: MessageEvent) => {
118
+ // if the messageEvent is a no-reply, we ignore it and stop loading
119
+ if (messageEvent.message.messageType !== MessageType.noReply) {
120
+ this.rawPubnubMessages = [
121
+ ...this.rawPubnubMessages,
122
+ {
123
+ channel: messageEvent.channel,
124
+ message: messageEvent.message,
125
+ publisher: messageEvent.publisher,
126
+ subscription: messageEvent.subscription,
127
+ timetoken: +messageEvent.timetoken,
128
+ },
129
+ ];
130
+ this.simpleChatMessages =
131
+ this.translatePubnubMessagesIntoSimpleChatMessages(
132
+ this.rawPubnubMessages
133
+ );
134
+ }
135
+
136
+ const isWaitingForEliseResponse =
137
+ messageEvent.publisher !== "eliseai" &&
138
+ messageEvent.publisher !== "elise_health_ai";
139
+ this.chatListener?.({
140
+ messages: this.simpleChatMessages,
141
+ isLoading: isWaitingForEliseResponse,
142
+ });
143
+ if (!isWaitingForEliseResponse && this.eliseResponseTimeout) {
144
+ clearTimeout(this.eliseResponseTimeout);
145
+ }
146
+ this.isLoadingMessages = isWaitingForEliseResponse;
147
+
148
+ if (messageEvent.message.customType === "ai_message") {
149
+ const containsConfirmedTour = this.simpleChatMessages.some(
150
+ (message) => {
151
+ return (
152
+ message.type === SimpleMessageTypes.text &&
153
+ message.message.includes("confirmed for your tour")
154
+ );
155
+ }
156
+ );
157
+ if (containsConfirmedTour) {
158
+ pushGtmEvent("scheduledTourEvent", {
159
+ tourDetails: messageEvent.message,
160
+ buildingId: this.building?.id,
161
+ buildingSlug: this.buildingSlug,
162
+ orgSlug: this.orgSlug,
163
+ leadUserId: this.leadUserId,
164
+ channel: this.channel,
165
+ });
166
+ }
167
+ }
168
+
169
+ this.checkAndHandleGTMForLeadEmail(messageEvent);
170
+ },
171
+ };
172
+ isLoadingMessages = false;
173
+ isFirstChatMessageSent = false;
174
+
175
+ constructor(
176
+ buildingSlug: string,
177
+ buildingDetails: Building | null,
178
+ orgSlug: string,
179
+ leadSource: string | null = null,
180
+ leadUserId: string,
181
+ leadSourceClient: LeadSourceClient | null = null
182
+ ) {
183
+ this.buildingSlug = buildingSlug;
184
+ this.building = buildingDetails;
185
+ this.orgSlug = orgSlug;
186
+ this.leadSource = leadSource;
187
+ this.leadUserId = leadUserId;
188
+ this.channel = `webchat_${leadUserId}`;
189
+ this.leadSourceClient = leadSourceClient;
190
+ }
191
+
192
+ checkAndHandleGTMForLeadEmail = (messageEvent: Pubnub.MessageEvent): void => {
193
+ try {
194
+ if (messageEvent.message.customType !== "lead_message") {
195
+ return;
196
+ }
197
+ if (!isContainingEmail(messageEvent.message.text)) {
198
+ return;
199
+ }
200
+ pushGtmEvent("leadProvidedEmail", {
201
+ buildingId: this.building?.id,
202
+ buildingSlug: this.buildingSlug,
203
+ orgSlug: this.orgSlug,
204
+ leadUserId: this.leadUserId,
205
+ channel: this.channel,
206
+ });
207
+ } catch (error) {
208
+ return;
209
+ }
210
+ };
211
+
212
+ addChatListener(
213
+ listener: (response: {
214
+ messages: SimpleChatMessage[];
215
+ isLoading: boolean;
216
+ }) => void
217
+ ): void {
218
+ this.chatListener = listener;
219
+ }
220
+
221
+ async initializePubnub(
222
+ chatStorageKey: ChatStorageKey
223
+ ): Promise<Pubnub | undefined> {
224
+ if (!chatStorageKey.leadId) return;
225
+ this.leadUserId = chatStorageKey.leadId;
226
+
227
+ const pubnubToken = await this.fetchToken(this.leadUserId, this.channel);
228
+ if (!pubnubToken) return;
229
+
230
+ // These keys are OK to expose live, the authKey generated by the BE is what
231
+ // is used to authenticate the user. Ideally, should also add rate limiting
232
+ // and/or IP whitelisting to the BE endpoint that generates the token!!
233
+ this.pubnub = new Pubnub({
234
+ publishKey: pubnubToken.keys.publish_key,
235
+ subscribeKey: pubnubToken.keys.subscribe_key,
236
+ userId: this.leadUserId,
237
+ authKey: pubnubToken.auth.result.token,
238
+ origin: "meetelise.pubnubapi.com",
239
+ });
240
+ this.withAuthToken(() => new Promise(() => this.handleChatListeners()));
241
+ await this.withAuthToken(() => this.getChannelHistory());
242
+ return this.pubnub;
243
+ }
244
+ async fetchToken(
245
+ lead: string,
246
+ channel: string
247
+ ): Promise<TokenResponse | null> {
248
+ try {
249
+ const response = await axios.get(
250
+ `${
251
+ this.apiHost
252
+ }/platformApi/webchat/pn/request-token?user_id=${lead}&channel=${channel}&${
253
+ this.building ? "" : `industry=health_care`
254
+ }`
255
+ );
256
+ return response.data;
257
+ } catch (error) {
258
+ if (this.building) {
259
+ sendLoggingEvent({
260
+ logTitle: "PUBNUB_ERROR_FETCHING_TOKEN",
261
+ logData: { error },
262
+ logType: LogType.error,
263
+ buildingSlug: this.buildingSlug,
264
+ orgSlug: this.orgSlug,
265
+ });
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ async fetchChannelExists(channel: string): Promise<boolean> {
271
+ try {
272
+ const response = await axios.get(
273
+ `${this.apiHost}/platformApi/webchat/check-channel-exists?channel_name=${channel}`
274
+ );
275
+ return response.data;
276
+ } catch (error) {
277
+ if (this.building) {
278
+ sendLoggingEvent({
279
+ logTitle: "PUBNUB_ERROR_FETCHING_CHANNEL_EXISTS",
280
+ logData: { error },
281
+ logType: LogType.error,
282
+ buildingSlug: this.buildingSlug,
283
+ orgSlug: this.orgSlug,
284
+ });
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+
290
+ async withAuthToken(apiRequestFunc: () => Promise<void>): Promise<void> {
291
+ try {
292
+ await apiRequestFunc();
293
+ } catch (error: unknown) {
294
+ // only want to retry with new token if the error is a 403
295
+ if (
296
+ error instanceof AxiosError &&
297
+ error &&
298
+ error.response &&
299
+ error.response.status === 403
300
+ ) {
301
+ try {
302
+ if (!this.pubnub || !this.leadUserId || !this.channel) return;
303
+
304
+ const newToken = await this.fetchToken(this.leadUserId, this.channel);
305
+ if (!newToken) return;
306
+
307
+ this.pubnub.setAuthKey(newToken.auth.result.token);
308
+
309
+ await apiRequestFunc();
310
+ } catch (retryError) {
311
+ if (this.building) {
312
+ sendLoggingEvent({
313
+ logTitle: "PUBNUB_ERROR_REFETCHING_TOKEN",
314
+ logData: {
315
+ retryError,
316
+ },
317
+ logType: LogType.error,
318
+ buildingSlug: this.buildingSlug,
319
+ orgSlug: this.orgSlug,
320
+ });
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ async getChannelHistory(maxTotalMessageChunksToFetch = 1500): Promise<void> {
328
+ try {
329
+ let allMessages: RawPubnubMessage[] = [];
330
+ let startTimeToken: string | number | null = null;
331
+ const maxCountPerFetch = 100;
332
+ for (
333
+ let totalCount = 0;
334
+ totalCount < maxTotalMessageChunksToFetch;
335
+ totalCount += maxCountPerFetch
336
+ ) {
337
+ const response: Pubnub.FetchMessagesResponse = await new Promise(
338
+ (resolve, reject) => {
339
+ if (!this.pubnub || !this.channel) return [];
340
+ const countToFetch = Math.min(
341
+ maxCountPerFetch,
342
+ maxTotalMessageChunksToFetch - totalCount
343
+ );
344
+ this.pubnub.fetchMessages(
345
+ {
346
+ channels: [this.channel],
347
+ count: countToFetch,
348
+ start: startTimeToken ?? undefined,
349
+ },
350
+ (status, response) => {
351
+ if (status.error) {
352
+ reject(status);
353
+ } else {
354
+ resolve(response);
355
+ }
356
+ }
357
+ );
358
+ }
359
+ );
360
+
361
+ const messages = response.channels[this.channel];
362
+ if (!messages) {
363
+ break;
364
+ }
365
+ messages.sort((a, b) => +a.timetoken - +b.timetoken);
366
+
367
+ if (!messages || messages.length === 0) {
368
+ break;
369
+ }
370
+ if (this.channel && Object.keys(response.channels).length !== 0) {
371
+ const currentChannelMessages = response.channels[this.channel];
372
+ const parsedCurrentChannelMessages: RawPubnubMessage[] = [];
373
+ currentChannelMessages.forEach((message) => {
374
+ if (message.uuid) {
375
+ parsedCurrentChannelMessages.push({
376
+ channel: message.channel,
377
+ message: message.message,
378
+ publisher: message.uuid,
379
+ subscription: message.channel,
380
+ timetoken: +message.timetoken,
381
+ });
382
+ }
383
+ });
384
+ allMessages = allMessages.concat(
385
+ parsedCurrentChannelMessages.filter(
386
+ (m) => m.message.messageType !== MessageType.noReply
387
+ )
388
+ );
389
+ }
390
+ startTimeToken = messages[0].timetoken;
391
+
392
+ if (
393
+ allMessages.length >= maxTotalMessageChunksToFetch ||
394
+ messages.length < maxCountPerFetch
395
+ ) {
396
+ break;
397
+ }
398
+ }
399
+ this.rawPubnubMessages = allMessages.slice(
400
+ 0,
401
+ maxTotalMessageChunksToFetch
402
+ );
403
+
404
+ this.simpleChatMessages =
405
+ this.translatePubnubMessagesIntoSimpleChatMessages(
406
+ this.rawPubnubMessages
407
+ );
408
+
409
+ this.chatListener?.({
410
+ messages: this.simpleChatMessages,
411
+ isLoading: false,
412
+ });
413
+ } catch (error) {
414
+ if (this.building) {
415
+ sendLoggingEvent({
416
+ logTitle: "PUBNUB_WARN_FETCHING_HISTORY",
417
+ logData: { error },
418
+ logType: LogType.warn,
419
+ buildingSlug: this.buildingSlug,
420
+ orgSlug: this.orgSlug,
421
+ });
422
+ }
423
+ }
424
+ }
425
+
426
+ handleChatListeners = (): void => {
427
+ if (!this.pubnub || !this.channel) return;
428
+ try {
429
+ this.pubnub.subscribe({ channels: [this.channel] });
430
+ this.pubnub.addListener(this.listenerParams);
431
+ } catch (error) {
432
+ if (this.building) {
433
+ sendLoggingEvent({
434
+ logTitle: "PUBNUB_ERROR_ADDING_LISTENER",
435
+ logData: {
436
+ error,
437
+ channel: this.channel,
438
+ leadUserId: this.leadUserId,
439
+ website: location.href,
440
+ },
441
+ logType: LogType.error,
442
+ buildingSlug: this.buildingSlug,
443
+ orgSlug: this.orgSlug,
444
+ });
445
+ }
446
+ }
447
+ };
448
+ handleDisconnect = (): void => {
449
+ if (this.eliseResponseTimeout) {
450
+ clearTimeout(this.eliseResponseTimeout);
451
+ this.eliseResponseTimeout = null;
452
+ }
453
+ this.removeChatListeners();
454
+ };
455
+ removeChatListeners = (): void => {
456
+ if (this.pubnub && this.channel) {
457
+ this.pubnub.unsubscribe({ channels: [this.channel] });
458
+ this.pubnub.removeListener(this.listenerParams);
459
+ }
460
+ };
461
+
462
+ sendMessage = async (message: string): Promise<void> => {
463
+ if (message) {
464
+ if (!this.pubnub) {
465
+ // ONLY create/gets a chat session if user actually wants to chat
466
+ const chatStorageKey = createChatStorageKey(
467
+ this.buildingSlug,
468
+ true,
469
+ this.leadUserId
470
+ );
471
+ const myPubnub = await this.initializePubnub(chatStorageKey);
472
+ if (!myPubnub) return;
473
+ }
474
+
475
+ await this.withAuthToken(async () => {
476
+ if (!this.pubnub || !this.channel) return;
477
+
478
+ if (this.eliseResponseTimeout) {
479
+ clearTimeout(this.eliseResponseTimeout);
480
+ this.eliseResponseTimeout = null;
481
+ }
482
+ this.eliseResponseTimeout = setTimeout(() => {
483
+ // eslint-disable-next-line no-console
484
+ console.error("Elise AI did not respond in time...");
485
+ if (this.building) {
486
+ sendLoggingEvent({
487
+ logTitle: "PUBNUB_ERROR_ELISEAI_MESSAGE_TIMEOUT",
488
+ logData: {
489
+ channel: this.channel,
490
+ leadUserId: this.leadUserId,
491
+ message,
492
+ },
493
+ logType: LogType.error,
494
+ buildingSlug: this.buildingSlug,
495
+ orgSlug: this.orgSlug,
496
+ });
497
+ }
498
+ }, 90000); // if after 90 seconds, no message - we log error
499
+
500
+ if (this.simpleChatMessages.length === 0) {
501
+ this.leadSourceClient?.checkAndHandleForLogLeadSource({
502
+ webchatAction: "chat",
503
+ stateId: null,
504
+ });
505
+ }
506
+ await this.pubnub.publish({
507
+ channel: this.channel,
508
+ message: {
509
+ text: message,
510
+ customType: "lead_message",
511
+ buildingId: this.building?.id,
512
+ buildingSlug: this.buildingSlug,
513
+ userId: this.building?.userId, // this userid is actually the AI user!
514
+ leadSource: this.leadSource,
515
+ isDevState: this.shouldCreateAsDevState(),
516
+ },
517
+ });
518
+
519
+ if (!this.isFirstChatMessageSent) {
520
+ pushGtmEvent("firstChatMessageSent", {
521
+ message,
522
+ buildingId: this.building?.id,
523
+ buildingSlug: this.buildingSlug,
524
+ orgSlug: this.orgSlug,
525
+ leadUserId: this.leadUserId,
526
+ channel: this.channel,
527
+ });
528
+ this.isFirstChatMessageSent = true;
529
+ }
530
+
531
+ pushGtmEvent("chatMessageSent", {
532
+ message,
533
+ buildingId: this.building?.id,
534
+ buildingSlug: this.buildingSlug,
535
+ orgSlug: this.orgSlug,
536
+ leadUserId: this.leadUserId,
537
+ channel: this.channel,
538
+ });
539
+ });
540
+
541
+ if (this.isLoadingMessages === false) this.isLoadingMessages = true;
542
+ }
543
+ };
544
+ private shouldCreateAsDevState(): boolean {
545
+ return location.href.startsWith(
546
+ "https://app.meetelise.com/settings/widgets"
547
+ );
548
+ }
549
+
550
+ isLeadMessage = (message: RawPubnubMessage): boolean =>
551
+ message.publisher.includes("lead_") &&
552
+ message.message.customType === "lead_message";
553
+
554
+ translatePubnubMessagesIntoSimpleChatMessages = (
555
+ messages: RawPubnubMessage[]
556
+ ): SimpleChatMessage[] => {
557
+ const parsedMessages: SimpleChatMessage[] = [];
558
+ const streamingIdToMessageChunks: {
559
+ [streamId: string]: RawPubnubMessage[];
560
+ } = {};
561
+ messages.forEach((message: RawPubnubMessage) => {
562
+ // Translate media messages
563
+ if (message.message.messageType === "media") {
564
+ parsedMessages.push({
565
+ timestamp: message.timetoken,
566
+ media: message.message.media ?? [],
567
+ isLeadMessage: this.isLeadMessage(message),
568
+ type: SimpleMessageTypes.media,
569
+ });
570
+ } else if (
571
+ // Translate non-streaming messages
572
+ !message.message.stream_id ||
573
+ (message.message.is_done && message.message.order === 0)
574
+ ) {
575
+ parsedMessages.push({
576
+ timestamp: message.timetoken,
577
+ message: message.message.text,
578
+ isLeadMessage: this.isLeadMessage(message),
579
+ chunks: [
580
+ {
581
+ text: message.message.text,
582
+ order: 0,
583
+ isDone: true,
584
+ },
585
+ ],
586
+ type: SimpleMessageTypes.text,
587
+ });
588
+ } else {
589
+ // Translate streaming messages
590
+ if (streamingIdToMessageChunks[message.message.stream_id]) {
591
+ streamingIdToMessageChunks[message.message.stream_id].push(message);
592
+ } else {
593
+ streamingIdToMessageChunks[message.message.stream_id] = [message];
594
+ }
595
+ }
596
+ });
597
+ Object.keys(streamingIdToMessageChunks).forEach((streamId) => {
598
+ const messages = streamingIdToMessageChunks[streamId];
599
+ const sortedChunks = this.getConsecutiveChunks(
600
+ messages.sort((a, b) => (a.message.order ?? 0) - (b.message.order ?? 0))
601
+ );
602
+ const text = sortedChunks.map((message) => message.message.text).join("");
603
+ const firstMessage = sortedChunks[0];
604
+ const newMessage: SimpleTextChatMessage = {
605
+ timestamp: firstMessage.timetoken,
606
+ message: text,
607
+ isLeadMessage: this.isLeadMessage(firstMessage),
608
+ chunks: sortedChunks.map((message) => ({
609
+ text: message.message.text,
610
+ order: message.message.order ?? 0,
611
+ isDone: message.message.is_done ?? false,
612
+ })),
613
+ type: SimpleMessageTypes.text,
614
+ };
615
+ this.isCurrentlyStreamingMessage =
616
+ this.isMessageStillStreamingChunks(sortedChunks);
617
+ parsedMessages.push(newMessage);
618
+ });
619
+ parsedMessages.sort((a, b) => a.timestamp - b.timestamp);
620
+ return parsedMessages;
621
+ };
622
+
623
+ private getConsecutiveChunks = (
624
+ rawPubnubMessages: RawPubnubMessage[]
625
+ ): RawPubnubMessage[] => {
626
+ const sortedRawPubnubMessages = rawPubnubMessages.sort(
627
+ (a, b) => (a.message.order ?? 0) - (b.message.order ?? 0)
628
+ );
629
+ const consecutiveMessages = [];
630
+
631
+ // get the LOWEST order that exists
632
+ let expectedOrder = sortedRawPubnubMessages[0].message.order ?? 0;
633
+ for (const message of sortedRawPubnubMessages) {
634
+ if ((message.message.order ?? -1) === expectedOrder) {
635
+ consecutiveMessages.push(message);
636
+ expectedOrder++;
637
+ } else {
638
+ break;
639
+ }
640
+ }
641
+ return consecutiveMessages;
642
+ };
643
+
644
+ private isMessageStillStreamingChunks = (
645
+ rawPubnubMessages: RawPubnubMessage[]
646
+ ): boolean => {
647
+ const messageChunkMarkedAsDone = rawPubnubMessages.find(
648
+ (message) => message.message.is_done
649
+ );
650
+ if (!messageChunkMarkedAsDone) return true;
651
+
652
+ // We check to see if ALL the chunks have been received
653
+ return rawPubnubMessages.length === messageChunkMarkedAsDone.message.order;
654
+ };
655
+ }
656
+
657
+ export default MyPubnub;