@nyaruka/temba-components 0.132.0 → 0.134.0

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 (181) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/demo/components/flow/example.html +1 -0
  3. package/demo/components/webchat/example.html +1 -1
  4. package/demo/static/css/tailwind.css +30019 -0
  5. package/dist/locales/es.js +5 -5
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +5 -5
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/locale-codes.js +2 -11
  10. package/dist/locales/locale-codes.js.map +1 -1
  11. package/dist/locales/pt.js +5 -5
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +555 -476
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/display/Chat.js +248 -95
  16. package/out-tsc/src/display/Chat.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +4 -4
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/display/TembaUser.js +3 -3
  20. package/out-tsc/src/display/TembaUser.js.map +1 -1
  21. package/out-tsc/src/events.js.map +1 -1
  22. package/out-tsc/src/flow/CanvasNode.js +132 -58
  23. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  24. package/out-tsc/src/flow/Editor.js +183 -58
  25. package/out-tsc/src/flow/Editor.js.map +1 -1
  26. package/out-tsc/src/flow/utils.js +141 -0
  27. package/out-tsc/src/flow/utils.js.map +1 -1
  28. package/out-tsc/src/interfaces.js.map +1 -1
  29. package/out-tsc/src/layout/FloatingWindow.js +1 -2
  30. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  31. package/out-tsc/src/list/ContentMenu.js +1 -0
  32. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  33. package/out-tsc/src/list/SortableList.js +3 -2
  34. package/out-tsc/src/list/SortableList.js.map +1 -1
  35. package/out-tsc/src/live/ContactChat.js +184 -205
  36. package/out-tsc/src/live/ContactChat.js.map +1 -1
  37. package/out-tsc/src/locales/es.js +5 -5
  38. package/out-tsc/src/locales/es.js.map +1 -1
  39. package/out-tsc/src/locales/fr.js +5 -5
  40. package/out-tsc/src/locales/fr.js.map +1 -1
  41. package/out-tsc/src/locales/locale-codes.js +2 -11
  42. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  43. package/out-tsc/src/locales/pt.js +5 -5
  44. package/out-tsc/src/locales/pt.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +34 -0
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/src/store/Store.js +5 -5
  48. package/out-tsc/src/store/Store.js.map +1 -1
  49. package/out-tsc/src/utils.js +3 -3
  50. package/out-tsc/src/utils.js.map +1 -1
  51. package/out-tsc/src/webchat/WebChat.js +22 -9
  52. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  53. package/out-tsc/test/ActionHelper.js +6 -5
  54. package/out-tsc/test/ActionHelper.js.map +1 -1
  55. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  56. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  57. package/out-tsc/test/temba-contact-chat.test.js +1 -1
  58. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  59. package/out-tsc/test/temba-floating-window.test.js +0 -2
  60. package/out-tsc/test/temba-floating-window.test.js.map +1 -1
  61. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  62. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  63. package/out-tsc/test/temba-flow-editor-node.test.js +195 -0
  64. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  65. package/out-tsc/test/temba-utils-uuid.test.js +45 -1
  66. package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
  67. package/out-tsc/test/utils.test.js +2 -2
  68. package/out-tsc/test/utils.test.js.map +1 -1
  69. package/package.json +1 -1
  70. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  71. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  72. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  73. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  74. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  75. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  76. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  77. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  78. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  79. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  80. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  81. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  82. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  83. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  84. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  85. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  89. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  90. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  91. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  92. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  93. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  94. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  95. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  96. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  97. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  98. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  99. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  100. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  101. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  102. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  103. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  104. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  105. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  106. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  107. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  108. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  109. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  110. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  112. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  113. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  114. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  115. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  116. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  117. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  118. package/screenshots/truth/contacts/chat-failure.png +0 -0
  119. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  120. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  121. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  122. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  123. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  124. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  125. package/screenshots/truth/floating-tab/default.png +0 -0
  126. package/screenshots/truth/floating-tab/gray.png +0 -0
  127. package/screenshots/truth/floating-tab/green.png +0 -0
  128. package/screenshots/truth/floating-tab/hover.png +0 -0
  129. package/screenshots/truth/floating-tab/purple.png +0 -0
  130. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  131. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  132. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  133. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  134. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  135. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  136. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  137. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  138. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  139. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  140. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  141. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  142. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  143. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  152. package/src/display/Chat.ts +331 -135
  153. package/src/display/FloatingTab.ts +4 -4
  154. package/src/display/TembaUser.ts +3 -2
  155. package/src/events.ts +12 -12
  156. package/src/flow/CanvasNode.ts +140 -57
  157. package/src/flow/Editor.ts +240 -58
  158. package/src/flow/utils.ts +207 -1
  159. package/src/interfaces.ts +7 -0
  160. package/src/layout/FloatingWindow.ts +1 -3
  161. package/src/list/ContentMenu.ts +1 -0
  162. package/src/list/SortableList.ts +3 -2
  163. package/src/live/ContactChat.ts +195 -221
  164. package/src/locales/es.ts +13 -18
  165. package/src/locales/fr.ts +13 -18
  166. package/src/locales/locale-codes.ts +2 -11
  167. package/src/locales/pt.ts +13 -18
  168. package/src/store/AppState.ts +43 -0
  169. package/src/store/Store.ts +5 -5
  170. package/src/utils.ts +3 -3
  171. package/src/webchat/WebChat.ts +24 -10
  172. package/test/ActionHelper.ts +13 -5
  173. package/test/actions/send_broadcast.test.ts +4 -2
  174. package/test/temba-contact-chat.test.ts +1 -1
  175. package/test/temba-floating-window.test.ts +0 -2
  176. package/test/temba-flow-collision.test.ts +833 -0
  177. package/test/temba-flow-editor-node.test.ts +224 -0
  178. package/test/temba-utils-uuid.test.ts +61 -1
  179. package/test/utils.test.ts +7 -2
  180. package/test-assets/contacts/history.json +22 -9
  181. package/web-test-runner.config.mjs +3 -3
@@ -3,7 +3,7 @@ import { __decorate } from "tslib";
3
3
  import { css, html } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
5
  import { CustomEventType } from '../interfaces';
6
- import { fetchResults, getUrl, oxfordFn, postJSON, postUrl } from '../utils';
6
+ import { fetchResults, generateUUIDv7, getUrl, oxfordFn, postJSON, postUrl } from '../utils';
7
7
  import { ContactStoreElement } from './ContactStoreElement';
8
8
  import { MessageType } from '../display/Chat';
9
9
  import { DEFAULT_AVATAR } from '../webchat/assets';
@@ -123,17 +123,37 @@ const renderContactURNsChanged = (event) => {
123
123
  export const renderTicketAction = (event, action) => {
124
124
  var _a;
125
125
  const ticketUUID = ((_a = event.ticket) === null || _a === void 0 ? void 0 : _a.uuid) || event.ticket_uuid;
126
- if (event._user) {
127
- return html `<div>
128
- <strong>${event._user.name}</strong> ${action} a
129
- <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
130
- </div>`;
126
+ const actionNote = event.note
127
+ ? html `<div
128
+ style="width:85%; background: #fffac3; padding: 1em;margin-bottom: 1em 0; border: 1px solid #ffe97f;border-radius: var(--curvature);"
129
+ >
130
+ <div style="color: #8e830fff; font-size: 1em;margin-bottom:0.25em">
131
+ <strong>${event._user.name}</strong> added a note
132
+ <temba-date
133
+ value=${event.created_on.toISOString()}
134
+ display="relative"
135
+ ></temba-date>
136
+ </div>
137
+ ${event.note}
138
+ </div>`
139
+ : null;
140
+ if (action === 'noted') {
141
+ return html `${actionNote}`;
131
142
  }
132
- return html `<div>
133
- A
134
- <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong> was
135
- <strong>${action}</strong>
136
- </div>`;
143
+ const description = event._user
144
+ ? html `<div>
145
+ <strong>${event._user.name}</strong> ${action} a
146
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
147
+ </div>`
148
+ : html `<div>
149
+ A
150
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
151
+ was <strong>${action}</strong>
152
+ </div>`;
153
+ return html `<div style="${actionNote ? 'margin-bottom: 1em;' : ''}">
154
+ ${description}
155
+ </div>
156
+ ${actionNote}`;
137
157
  };
138
158
  export const renderTicketAssigneeChanged = (event) => {
139
159
  if (event._user) {
@@ -427,10 +447,12 @@ export class ContactChat extends ContactStoreElement {
427
447
  this.showInterrupt = false;
428
448
  this.avatar = DEFAULT_AVATAR;
429
449
  this.ticket = null;
430
- this.lastEventTime = null;
431
- this.newestEventTime = null;
450
+ this.beforeUUID = null; // for scrolling back through history
451
+ this.afterUUID = null; // for polling new messages
432
452
  this.refreshId = null;
433
453
  this.polling = false;
454
+ this.pollingInterval = 2000; // start at 2 seconds
455
+ this.lastFetchTime = null;
434
456
  }
435
457
  firstUpdated(changed) {
436
458
  super.firstUpdated(changed);
@@ -446,6 +468,7 @@ export class ContactChat extends ContactStoreElement {
446
468
  }
447
469
  }
448
470
  updated(changedProperties) {
471
+ var _a;
449
472
  super.updated(changedProperties);
450
473
  // if we don't have an endpoint infer one
451
474
  if (changedProperties.has('data') ||
@@ -457,9 +480,15 @@ export class ContactChat extends ContactStoreElement {
457
480
  }
458
481
  this.currentContact = this.data;
459
482
  }
460
- if (changedProperties.has('currentContact')) {
483
+ if (changedProperties.has('currentContact') && this.currentContact) {
461
484
  this.chat = this.shadowRoot.querySelector('temba-chat');
462
- this.reset();
485
+ if (this.currentContact.uuid !==
486
+ ((_a = changedProperties.get('currentContact')) === null || _a === void 0 ? void 0 : _a.uuid)) {
487
+ this.reset();
488
+ }
489
+ else {
490
+ setTimeout(() => this.checkForNewMessages(), 500);
491
+ }
463
492
  this.fetchPreviousMessages();
464
493
  }
465
494
  }
@@ -469,10 +498,12 @@ export class ContactChat extends ContactStoreElement {
469
498
  }
470
499
  this.blockFetching = false;
471
500
  this.ticket = null;
472
- this.lastEventTime = null;
473
- this.newestEventTime = null;
501
+ this.beforeUUID = null;
502
+ this.afterUUID = null;
474
503
  this.refreshId = null;
475
504
  this.polling = false;
505
+ this.pollingInterval = 2000;
506
+ this.lastFetchTime = null;
476
507
  this.errorMessage = null;
477
508
  const compose = this.shadowRoot.querySelector('temba-compose');
478
509
  if (compose) {
@@ -511,8 +542,11 @@ export class ContactChat extends ContactStoreElement {
511
542
  postJSON(`/contact/chat/${this.currentContact.uuid}/`, payload)
512
543
  .then((response) => {
513
544
  if (response.status < 400) {
514
- const msg = this.createChatForMessageEvent(response.json.event);
515
- this.chat.addMessages([msg], null, true);
545
+ const event = response.json.event;
546
+ event.created_on = new Date(event.created_on);
547
+ this.chat.addMessages([event], null, true);
548
+ // reset polling interval to 2 seconds after sending a message
549
+ this.pollingInterval = 2000;
516
550
  this.checkForNewMessages();
517
551
  composeEle.reset();
518
552
  this.fireCustomEvent(CustomEventType.MessageSent, {
@@ -530,240 +564,174 @@ export class ContactChat extends ContactStoreElement {
530
564
  }
531
565
  getEndpoint() {
532
566
  if (this.contact) {
533
- return `/contact/history/${this.contact}/?_format=json`;
567
+ return `/contact/chat/${this.contact}/`;
534
568
  }
535
569
  return null;
536
570
  }
537
- scheduleRefresh() {
538
- // knock five seconds off the newest event time so we are
539
- // a little more aggressive about refreshing short term
540
- let window = new Date().getTime() - this.newestEventTime / 1000 - 5000;
571
+ scheduleRefresh(hasNewEvents = false) {
541
572
  if (this.refreshId) {
542
573
  clearTimeout(this.refreshId);
543
574
  this.refreshId = null;
544
575
  }
545
- // wait no longer than 15 seconds
546
- window = Math.min(window, 15000);
547
- // wait at least 2 seconds
548
- window = Math.max(window, 2000);
576
+ // reset to 2 seconds if we received new events
577
+ if (hasNewEvents) {
578
+ this.pollingInterval = 2000;
579
+ }
580
+ else {
581
+ // increase interval by 1 second up to max of 15 seconds
582
+ this.pollingInterval = Math.min(this.pollingInterval + 1000, 15000);
583
+ }
549
584
  this.refreshId = setTimeout(() => {
550
585
  this.checkForNewMessages();
551
- }, window);
586
+ }, this.pollingInterval);
552
587
  }
553
- getEventMessage(event) {
554
- let message = null;
588
+ prerender(event) {
555
589
  switch (event.type) {
556
590
  case Events.AIRTIME_TRANSFERRED:
557
- message = {
558
- type: MessageType.Inline,
559
- text: renderAirtimeTransferredEvent(event)
591
+ event._rendered = {
592
+ html: renderAirtimeTransferredEvent(event),
593
+ type: MessageType.Inline
560
594
  };
561
595
  break;
562
596
  case Events.CALL_CREATED:
563
597
  case Events.CALL_MISSED:
564
598
  case Events.CALL_RECEIVED:
565
- message = {
566
- type: MessageType.Inline,
567
- text: renderCallEvent(event)
599
+ event._rendered = {
600
+ html: renderCallEvent(event),
601
+ type: MessageType.Inline
568
602
  };
569
603
  break;
570
604
  case Events.CHAT_STARTED:
571
- message = {
572
- type: MessageType.Inline,
573
- text: renderChatStartedEvent(event)
605
+ event._rendered = {
606
+ html: renderChatStartedEvent(event),
607
+ type: MessageType.Inline
574
608
  };
575
609
  break;
576
610
  case Events.CONTACT_FIELD_CHANGED:
577
- message = {
578
- type: MessageType.Inline,
579
- text: renderUpdateEvent(event)
611
+ event._rendered = {
612
+ html: renderUpdateEvent(event),
613
+ type: MessageType.Inline
580
614
  };
581
615
  break;
582
616
  case Events.CONTACT_GROUPS_CHANGED:
583
- message = {
584
- type: MessageType.Inline,
585
- text: renderContactGroupsEvent(event)
617
+ event._rendered = {
618
+ html: renderContactGroupsEvent(event),
619
+ type: MessageType.Inline
586
620
  };
587
621
  break;
588
622
  case Events.CONTACT_LANGUAGE_CHANGED:
589
- message = {
590
- type: MessageType.Inline,
591
- text: renderContactLanguageChangedEvent(event)
623
+ event._rendered = {
624
+ html: renderContactLanguageChangedEvent(event),
625
+ type: MessageType.Inline
592
626
  };
593
627
  break;
594
628
  case Events.CONTACT_NAME_CHANGED:
595
- message = {
596
- type: MessageType.Inline,
597
- text: renderNameChanged(event)
629
+ event._rendered = {
630
+ html: renderNameChanged(event),
631
+ type: MessageType.Inline
598
632
  };
599
633
  break;
600
634
  case Events.CONTACT_STATUS_CHANGED:
601
- message = {
602
- type: MessageType.Inline,
603
- text: renderContactStatusChangedEvent(event)
635
+ event._rendered = {
636
+ html: renderContactStatusChangedEvent(event),
637
+ type: MessageType.Inline
604
638
  };
605
639
  break;
606
640
  case Events.CONTACT_URNS_CHANGED:
607
- message = {
608
- type: MessageType.Inline,
609
- text: renderContactURNsChanged(event)
641
+ event._rendered = {
642
+ html: renderContactURNsChanged(event),
643
+ type: MessageType.Inline
610
644
  };
611
645
  break;
612
646
  case Events.OPTIN_REQUESTED:
613
647
  case Events.OPTIN_STARTED:
614
648
  case Events.OPTIN_STOPPED:
615
- message = {
616
- type: MessageType.Inline,
617
- text: renderOptInEvent(event)
649
+ event._rendered = {
650
+ html: renderOptInEvent(event),
651
+ type: MessageType.Inline
618
652
  };
619
653
  break;
620
654
  case Events.RUN_STARTED:
621
655
  case Events.RUN_ENDED:
622
- message = {
623
- type: MessageType.Inline,
624
- text: renderRunEvent(event)
656
+ event._rendered = {
657
+ html: renderRunEvent(event),
658
+ type: MessageType.Inline
625
659
  };
626
660
  break;
627
661
  case Events.TICKET_ASSIGNEE_CHANGED:
628
- message = {
629
- type: MessageType.Inline,
630
- text: renderTicketAssigneeChanged(event)
662
+ event._rendered = {
663
+ html: renderTicketAssigneeChanged(event),
664
+ type: MessageType.Inline
631
665
  };
632
666
  break;
633
667
  case Events.TICKET_CLOSED:
634
- message = {
635
- type: MessageType.Inline,
636
- text: renderTicketAction(event, 'closed')
668
+ event._rendered = {
669
+ html: renderTicketAction(event, 'closed'),
670
+ type: MessageType.Inline
637
671
  };
638
672
  break;
639
673
  case Events.TICKET_OPENED:
640
- message = {
641
- type: MessageType.Inline,
642
- text: renderTicketAction(event, 'opened')
674
+ event._rendered = {
675
+ html: renderTicketAction(event, 'opened'),
676
+ type: MessageType.Inline
677
+ };
678
+ break;
679
+ case Events.TICKET_NOTE_ADDED:
680
+ event._rendered = {
681
+ html: renderTicketAction(event, 'noted'),
682
+ type: MessageType.Inline
643
683
  };
644
684
  break;
645
685
  case Events.TICKET_REOPENED:
646
- message = {
647
- type: MessageType.Inline,
648
- text: renderTicketAction(event, 'reopened')
686
+ event._rendered = {
687
+ html: renderTicketAction(event, 'reopened'),
688
+ type: MessageType.Inline
649
689
  };
650
690
  break;
651
691
  case Events.TICKET_TOPIC_CHANGED:
652
- message = {
653
- type: MessageType.Inline,
654
- text: html `<div>
692
+ event._rendered = {
693
+ html: html `<div>
655
694
  Topic changed to
656
695
  <strong>${event.topic.name}</strong>
657
- </div>`
696
+ </div>`,
697
+ type: MessageType.Inline
658
698
  };
659
699
  break;
660
700
  case Events.CHANNEL_EVENT: // deprecated
661
- message = {
662
- type: MessageType.Inline,
663
- text: renderChannelEvent(event)
701
+ event._rendered = {
702
+ html: renderChannelEvent(event),
703
+ type: MessageType.Inline
664
704
  };
665
705
  break;
666
706
  default:
667
707
  console.error('Unknown event type', event);
668
708
  }
669
- if (message) {
670
- message.id = event.uuid;
671
- message.date = new Date(event.created_on);
672
- }
673
- return message;
674
- }
675
- getUserForEvent(event) {
676
- if (event.type === 'msg_received') {
677
- return {
678
- name: this.currentContact.name
679
- };
680
- }
681
- else if (event._user) {
682
- return event._user;
683
- }
684
- return null;
685
- }
686
- createChatForMessageEvent(msgEvent) {
687
- return {
688
- id: msgEvent.uuid,
689
- type: msgEvent.type === 'msg_received' ? 'msg_in' : 'msg_out',
690
- user: this.getUserForEvent(msgEvent),
691
- date: new Date(msgEvent.created_on),
692
- attachments: msgEvent.msg.attachments,
693
- text: msgEvent.msg.text,
694
- sendError: msgEvent._status &&
695
- (msgEvent._status.status === 'errored' ||
696
- msgEvent._status.status === 'failed'),
697
- popup: html `<div
698
- style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
699
- >
700
- <div style="justify-content:left;text-align:left">
701
- <temba-date
702
- value=${msgEvent.created_on}
703
- display="duration"
704
- ></temba-date>
705
-
706
- ${msgEvent.optin
707
- ? html `<div style="font-size:0.9em;color:#aaa">
708
- ${msgEvent.optin.name}
709
- </div>`
710
- : null}
711
- </div>
712
- ${msgEvent._logs_url
713
- ? html `<a style="margin-left:0.5em" href="${msgEvent._logs_url}"
714
- ><temba-icon name="log"></temba-icon
715
- ></a>`
716
- : null}
717
- </div> `
718
- };
719
709
  }
720
710
  createMessages(page) {
721
711
  if (page.events) {
722
- let messages = [];
712
+ const messages = [];
723
713
  page.events.forEach((event) => {
724
- const ts = new Date(event.created_on).getTime() * 1000;
725
- if (ts > this.newestEventTime) {
726
- this.newestEventTime = ts;
727
- }
728
- if (event.type === 'ticket_note_added') {
729
- const ticketEvent = event;
730
- messages.push({
731
- type: MessageType.Note,
732
- id: event.created_on + event.type,
733
- user: this.getUserForEvent(ticketEvent),
734
- date: new Date(ticketEvent.created_on),
735
- text: ticketEvent.note
736
- });
714
+ // track the UUID of the newest event for polling
715
+ if (!this.afterUUID ||
716
+ event.uuid.toLowerCase() > this.afterUUID.toLowerCase()) {
717
+ this.afterUUID = event.uuid;
737
718
  }
738
- else if (event.type === 'ticket_opened') {
739
- // ticket open events can have a note attached
740
- const ticketEvent = event;
741
- messages.push({
742
- type: MessageType.Note,
743
- id: event.created_on + event.type + '_note',
744
- user: this.getUserForEvent(ticketEvent),
745
- date: new Date(ticketEvent.created_on),
746
- text: ticketEvent.note
747
- });
748
- // but the opening of the ticket is a normal event
749
- messages.push(this.getEventMessage(event));
750
- }
751
- else if (event.type === 'msg_created' ||
719
+ // convert to dates
720
+ event.created_on = new Date(event.created_on);
721
+ if (event.type === 'msg_created' ||
752
722
  event.type === 'msg_received' ||
753
723
  event.type === 'ivr_created') {
754
- const msgEvent = event;
755
- messages.push(this.createChatForMessageEvent(msgEvent));
724
+ messages.push(event);
756
725
  }
757
726
  else {
758
- const msg = this.getEventMessage(event);
759
- if (msg) {
760
- messages.push(msg);
727
+ this.prerender(event);
728
+ if (event._rendered) {
729
+ messages.push(event);
761
730
  }
762
731
  }
763
732
  });
764
733
  // remove any messages we don't recognize
765
- messages = messages.filter((msg) => !!msg);
766
- return messages;
734
+ return messages.filter((msg) => !!msg);
767
735
  }
768
736
  return [];
769
737
  }
@@ -774,26 +742,26 @@ export class ContactChat extends ContactStoreElement {
774
742
  return;
775
743
  }
776
744
  const chat = this.chat;
777
- const contactChat = this;
778
- if (this.currentContact && this.newestEventTime) {
745
+ if (this.currentContact && this.afterUUID) {
779
746
  this.polling = true;
747
+ this.lastFetchTime = Date.now();
780
748
  const endpoint = this.getEndpoint();
781
749
  if (!endpoint) {
782
750
  return;
783
751
  }
784
752
  const fetchContact = this.currentContact.uuid;
785
- fetchContactHistory(false, endpoint, (_a = this.currentTicket) === null || _a === void 0 ? void 0 : _a.uuid, null, this.newestEventTime).then((page) => {
753
+ fetchContactHistory(endpoint, (_a = this.currentTicket) === null || _a === void 0 ? void 0 : _a.uuid, null, this.afterUUID).then((page) => {
754
+ const messages = this.createMessages(page);
755
+ messages.reverse();
786
756
  if (fetchContact === this.currentContact.uuid) {
787
- this.lastEventTime = page.next_before;
788
- const messages = this.createMessages(page);
789
- if (messages.length === 0) {
790
- contactChat.blockFetching = true;
791
- }
792
- messages.reverse();
757
+ const hasNewEvents = messages.length > 0;
793
758
  chat.addMessages(messages, null, true);
759
+ this.polling = false;
760
+ this.scheduleRefresh(hasNewEvents);
761
+ }
762
+ else {
763
+ this.polling = false;
794
764
  }
795
- this.polling = false;
796
- this.scheduleRefresh();
797
765
  });
798
766
  }
799
767
  }
@@ -810,13 +778,31 @@ export class ContactChat extends ContactStoreElement {
810
778
  if (!endpoint) {
811
779
  return;
812
780
  }
813
- fetchContactHistory(false, endpoint, (_a = this.currentTicket) === null || _a === void 0 ? void 0 : _a.uuid, this.lastEventTime).then((page) => {
814
- this.lastEventTime = page.next_before;
781
+ // initialize anchor UUID if not set (first fetch)
782
+ if (!this.beforeUUID && !this.afterUUID) {
783
+ // generate a UUID v7 for current time as the anchor
784
+ const anchorUUID = generateUUIDv7();
785
+ this.beforeUUID = anchorUUID;
786
+ this.afterUUID = anchorUUID;
787
+ }
788
+ fetchContactHistory(endpoint, (_a = this.currentTicket) === null || _a === void 0 ? void 0 : _a.uuid, this.beforeUUID, null).then((page) => {
815
789
  const messages = this.createMessages(page);
816
790
  messages.reverse();
817
791
  if (messages.length === 0) {
818
792
  contactChat.blockFetching = true;
819
793
  }
794
+ else if (page.next) {
795
+ // update beforeUUID for next fetch of older messages
796
+ this.beforeUUID = page.next;
797
+ }
798
+ else {
799
+ // no more history, mark end and show oldest event date
800
+ contactChat.blockFetching = true;
801
+ if (page.events && page.events.length > 0) {
802
+ const oldestEvent = page.events[page.events.length - 1];
803
+ chat.setEndOfHistory(new Date(oldestEvent.created_on));
804
+ }
805
+ }
820
806
  chat.addMessages(messages);
821
807
  this.scheduleRefresh();
822
808
  });
@@ -921,15 +907,13 @@ export class ContactChat extends ContactStoreElement {
921
907
  refreshTicket() {
922
908
  if (this.currentTicket) {
923
909
  fetchResults(`/api/v2/tickets.json?uuid=${this.currentTicket.uuid}`).then((values) => {
924
- this.store.resolveUsers(values, ['assignee']).then(() => {
925
- if (values.length > 0) {
926
- this.fireCustomEvent(CustomEventType.TicketUpdated, {
927
- ticket: values[0],
928
- previous: this.currentTicket
929
- });
930
- this.currentTicket = values[0];
931
- }
932
- });
910
+ if (values.length > 0) {
911
+ this.fireCustomEvent(CustomEventType.TicketUpdated, {
912
+ ticket: values[0],
913
+ previous: this.currentTicket
914
+ });
915
+ this.currentTicket = values[0];
916
+ }
933
917
  });
934
918
  }
935
919
  }
@@ -970,6 +954,7 @@ export class ContactChat extends ContactStoreElement {
970
954
  @temba-fetch-complete=${this.fetchComplete}
971
955
  avatar=${this.avatar}
972
956
  agent
957
+ ?hasFooter=${inFlow}
973
958
  >
974
959
  ${inFlow
975
960
  ? html `
@@ -1099,37 +1084,31 @@ export const fetchContact = (endpoint) => {
1099
1084
  });
1100
1085
  });
1101
1086
  };
1102
- export const fetchContactHistory = (reset, endpoint, ticket, before = undefined, after = undefined) => {
1103
- if (reset) {
1104
- pendingRequests.forEach((controller) => {
1105
- controller.abort();
1106
- });
1107
- pendingRequests = [];
1108
- }
1087
+ export const fetchContactHistory = (endpoint, ticket = undefined, before = undefined, after = undefined) => {
1109
1088
  return new Promise((resolve) => {
1110
1089
  const controller = new AbortController();
1111
1090
  pendingRequests.push(controller);
1112
1091
  let url = endpoint;
1092
+ const params = [];
1113
1093
  if (before) {
1114
- url += `&before=${before}`;
1094
+ params.push(`before=${before}`);
1115
1095
  }
1116
1096
  if (after) {
1117
- url += `&after=${after}`;
1097
+ params.push(`after=${after}`);
1118
1098
  }
1119
1099
  if (ticket) {
1120
- url += `&ticket=${ticket}`;
1100
+ params.push(`ticket=${ticket}`);
1101
+ }
1102
+ if (params.length > 0) {
1103
+ url += (url.includes('?') ? '&' : '?') + params.join('&');
1121
1104
  }
1122
- const store = document.querySelector('temba-store');
1123
1105
  getUrl(url, controller)
1124
1106
  .then((response) => {
1125
1107
  // on success, remove our abort controller
1126
1108
  pendingRequests = pendingRequests.filter((controller) => {
1127
1109
  return response.controller === controller;
1128
1110
  });
1129
- const page = response.json;
1130
- store.resolveUsers(page.events, ['created_by']).then(() => {
1131
- resolve(page);
1132
- });
1111
+ resolve(response.json);
1133
1112
  })
1134
1113
  .catch(() => {
1135
1114
  // canceled