@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
@@ -36,6 +36,7 @@ export class ContentMenu extends RapidElement {
36
36
  return css`
37
37
  :host {
38
38
  tabindex: 0;
39
+ z-index: 5000;
39
40
  }
40
41
  .container {
41
42
  --button-y: 0.4em;
@@ -577,8 +577,9 @@ export class SortableList extends RapidElement {
577
577
  const fromIdx = originalDragIdx;
578
578
  const toIdx = this.pendingDropIndex;
579
579
 
580
- // only fire if the position actually changed
581
- if (fromIdx !== toIdx) {
580
+ // only fire if the position actually changed AND this is not an external drag
581
+ // External drags are handled by external drop handlers
582
+ if (fromIdx !== toIdx && !this.isExternalDrag) {
582
583
  this.fireCustomEvent(CustomEventType.OrderChanged, {
583
584
  swap: [fromIdx, toIdx]
584
585
  });
@@ -10,6 +10,7 @@ import {
10
10
  } from '../interfaces';
11
11
  import {
12
12
  fetchResults,
13
+ generateUUIDv7,
13
14
  getUrl,
14
15
  oxfordFn,
15
16
  postJSON,
@@ -23,12 +24,10 @@ import {
23
24
  CallEvent,
24
25
  ChannelEvent,
25
26
  ChatStartedEvent,
26
- ContactEvent,
27
27
  ContactGroupsEvent,
28
28
  ContactHistoryPage,
29
29
  ContactLanguageChangedEvent,
30
30
  ContactStatusChangedEvent,
31
- MsgEvent,
32
31
  NameChangedEvent,
33
32
  OptInEvent,
34
33
  RunEvent,
@@ -36,11 +35,10 @@ import {
36
35
  UpdateFieldEvent,
37
36
  URNsChangedEvent
38
37
  } from '../events';
39
- import { Chat, ChatEvent, MessageType } from '../display/Chat';
38
+ import { Chat, MessageType, ContactEvent } from '../display/Chat';
40
39
  import { DEFAULT_AVATAR } from '../webchat/assets';
41
40
  import { UserSelect } from '../form/select/UserSelect';
42
41
  import { Select } from '../form/select/Select';
43
- import { Store } from '../store/Store';
44
42
 
45
43
  /*
46
44
  export const SCROLL_THRESHOLD = 100;
@@ -175,17 +173,40 @@ export const renderTicketAction = (
175
173
  ): TemplateResult => {
176
174
  const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
177
175
 
178
- if (event._user) {
179
- return html`<div>
180
- <strong>${event._user.name}</strong> ${action} a
181
- <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
182
- </div>`;
176
+ const actionNote = event.note
177
+ ? html`<div
178
+ style="width:85%; background: #fffac3; padding: 1em;margin-bottom: 1em 0; border: 1px solid #ffe97f;border-radius: var(--curvature);"
179
+ >
180
+ <div style="color: #8e830fff; font-size: 1em;margin-bottom:0.25em">
181
+ <strong>${event._user.name}</strong> added a note
182
+ <temba-date
183
+ value=${event.created_on.toISOString()}
184
+ display="relative"
185
+ ></temba-date>
186
+ </div>
187
+ ${event.note}
188
+ </div>`
189
+ : null;
190
+
191
+ if (action === 'noted') {
192
+ return html`${actionNote}`;
183
193
  }
184
- return html`<div>
185
- A
186
- <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong> was
187
- <strong>${action}</strong>
188
- </div>`;
194
+
195
+ const description = event._user
196
+ ? html`<div>
197
+ <strong>${event._user.name}</strong> ${action} a
198
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
199
+ </div>`
200
+ : html`<div>
201
+ A
202
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
203
+ was <strong>${action}</strong>
204
+ </div>`;
205
+
206
+ return html`<div style="${actionNote ? 'margin-bottom: 1em;' : ''}">
207
+ ${description}
208
+ </div>
209
+ ${actionNote}`;
189
210
  };
190
211
 
191
212
  export const renderTicketAssigneeChanged = (
@@ -525,10 +546,12 @@ export class ContactChat extends ContactStoreElement {
525
546
  private chat: Chat;
526
547
 
527
548
  ticket = null;
528
- lastEventTime = null;
529
- newestEventTime = null;
549
+ beforeUUID: string = null; // for scrolling back through history
550
+ afterUUID: string = null; // for polling new messages
530
551
  refreshId = null;
531
552
  polling = false;
553
+ pollingInterval = 2000; // start at 2 seconds
554
+ lastFetchTime: number = null;
532
555
 
533
556
  constructor() {
534
557
  super();
@@ -569,9 +592,16 @@ export class ContactChat extends ContactStoreElement {
569
592
  this.currentContact = this.data;
570
593
  }
571
594
 
572
- if (changedProperties.has('currentContact')) {
595
+ if (changedProperties.has('currentContact') && this.currentContact) {
573
596
  this.chat = this.shadowRoot.querySelector('temba-chat');
574
- this.reset();
597
+ if (
598
+ this.currentContact.uuid !==
599
+ changedProperties.get('currentContact')?.uuid
600
+ ) {
601
+ this.reset();
602
+ } else {
603
+ setTimeout(() => this.checkForNewMessages(), 500);
604
+ }
575
605
  this.fetchPreviousMessages();
576
606
  }
577
607
  }
@@ -582,10 +612,12 @@ export class ContactChat extends ContactStoreElement {
582
612
  }
583
613
  this.blockFetching = false;
584
614
  this.ticket = null;
585
- this.lastEventTime = null;
586
- this.newestEventTime = null;
615
+ this.beforeUUID = null;
616
+ this.afterUUID = null;
587
617
  this.refreshId = null;
588
618
  this.polling = false;
619
+ this.pollingInterval = 2000;
620
+ this.lastFetchTime = null;
589
621
  this.errorMessage = null;
590
622
 
591
623
  const compose = this.shadowRoot.querySelector('temba-compose') as Compose;
@@ -632,8 +664,11 @@ export class ContactChat extends ContactStoreElement {
632
664
  postJSON(`/contact/chat/${this.currentContact.uuid}/`, payload)
633
665
  .then((response) => {
634
666
  if (response.status < 400) {
635
- const msg = this.createChatForMessageEvent(response.json.event);
636
- this.chat.addMessages([msg], null, true);
667
+ const event = response.json.event;
668
+ event.created_on = new Date(event.created_on);
669
+ this.chat.addMessages([event], null, true);
670
+ // reset polling interval to 2 seconds after sending a message
671
+ this.pollingInterval = 2000;
637
672
  this.checkForNewMessages();
638
673
  composeEle.reset();
639
674
  this.fireCustomEvent(CustomEventType.MessageSent, {
@@ -651,257 +686,188 @@ export class ContactChat extends ContactStoreElement {
651
686
 
652
687
  private getEndpoint() {
653
688
  if (this.contact) {
654
- return `/contact/history/${this.contact}/?_format=json`;
689
+ return `/contact/chat/${this.contact}/`;
655
690
  }
656
691
  return null;
657
692
  }
658
693
 
659
- private scheduleRefresh() {
660
- // knock five seconds off the newest event time so we are
661
- // a little more aggressive about refreshing short term
662
- let window = new Date().getTime() - this.newestEventTime / 1000 - 5000;
663
-
694
+ private scheduleRefresh(hasNewEvents = false) {
664
695
  if (this.refreshId) {
665
696
  clearTimeout(this.refreshId);
666
697
  this.refreshId = null;
667
698
  }
668
699
 
669
- // wait no longer than 15 seconds
670
- window = Math.min(window, 15000);
671
-
672
- // wait at least 2 seconds
673
- window = Math.max(window, 2000);
700
+ // reset to 2 seconds if we received new events
701
+ if (hasNewEvents) {
702
+ this.pollingInterval = 2000;
703
+ } else {
704
+ // increase interval by 1 second up to max of 15 seconds
705
+ this.pollingInterval = Math.min(this.pollingInterval + 1000, 15000);
706
+ }
674
707
 
675
708
  this.refreshId = setTimeout(() => {
676
709
  this.checkForNewMessages();
677
- }, window);
710
+ }, this.pollingInterval);
678
711
  }
679
712
 
680
- public getEventMessage(event: ContactEvent): ChatEvent {
681
- let message = null;
713
+ public prerender(event: ContactEvent) {
682
714
  switch (event.type) {
683
715
  case Events.AIRTIME_TRANSFERRED:
684
- message = {
685
- type: MessageType.Inline,
686
- text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
716
+ event._rendered = {
717
+ html: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent),
718
+ type: MessageType.Inline
687
719
  };
688
720
  break;
689
721
  case Events.CALL_CREATED:
690
722
  case Events.CALL_MISSED:
691
723
  case Events.CALL_RECEIVED:
692
- message = {
693
- type: MessageType.Inline,
694
- text: renderCallEvent(event as CallEvent)
724
+ event._rendered = {
725
+ html: renderCallEvent(event as CallEvent),
726
+ type: MessageType.Inline
695
727
  };
696
728
  break;
697
729
  case Events.CHAT_STARTED:
698
- message = {
699
- type: MessageType.Inline,
700
- text: renderChatStartedEvent(event as ChatStartedEvent)
730
+ event._rendered = {
731
+ html: renderChatStartedEvent(event as ChatStartedEvent),
732
+ type: MessageType.Inline
701
733
  };
702
734
  break;
703
735
  case Events.CONTACT_FIELD_CHANGED:
704
- message = {
705
- type: MessageType.Inline,
706
- text: renderUpdateEvent(event as UpdateFieldEvent)
736
+ event._rendered = {
737
+ html: renderUpdateEvent(event as UpdateFieldEvent),
738
+ type: MessageType.Inline
707
739
  };
708
740
  break;
709
741
  case Events.CONTACT_GROUPS_CHANGED:
710
- message = {
711
- type: MessageType.Inline,
712
- text: renderContactGroupsEvent(event as ContactGroupsEvent)
742
+ event._rendered = {
743
+ html: renderContactGroupsEvent(event as ContactGroupsEvent),
744
+ type: MessageType.Inline
713
745
  };
714
746
  break;
715
747
  case Events.CONTACT_LANGUAGE_CHANGED:
716
- message = {
717
- type: MessageType.Inline,
718
- text: renderContactLanguageChangedEvent(
748
+ event._rendered = {
749
+ html: renderContactLanguageChangedEvent(
719
750
  event as ContactLanguageChangedEvent
720
- )
751
+ ),
752
+ type: MessageType.Inline
721
753
  };
722
754
  break;
723
755
  case Events.CONTACT_NAME_CHANGED:
724
- message = {
725
- type: MessageType.Inline,
726
- text: renderNameChanged(event as NameChangedEvent)
756
+ event._rendered = {
757
+ html: renderNameChanged(event as NameChangedEvent),
758
+ type: MessageType.Inline
727
759
  };
728
760
  break;
729
761
  case Events.CONTACT_STATUS_CHANGED:
730
- message = {
731
- type: MessageType.Inline,
732
- text: renderContactStatusChangedEvent(
762
+ event._rendered = {
763
+ html: renderContactStatusChangedEvent(
733
764
  event as ContactStatusChangedEvent
734
- )
765
+ ),
766
+ type: MessageType.Inline
735
767
  };
736
768
  break;
737
769
  case Events.CONTACT_URNS_CHANGED:
738
- message = {
739
- type: MessageType.Inline,
740
- text: renderContactURNsChanged(event as URNsChangedEvent)
770
+ event._rendered = {
771
+ html: renderContactURNsChanged(event as URNsChangedEvent),
772
+ type: MessageType.Inline
741
773
  };
742
774
  break;
743
775
  case Events.OPTIN_REQUESTED:
744
776
  case Events.OPTIN_STARTED:
745
777
  case Events.OPTIN_STOPPED:
746
- message = {
747
- type: MessageType.Inline,
748
- text: renderOptInEvent(event as OptInEvent)
778
+ event._rendered = {
779
+ html: renderOptInEvent(event as OptInEvent),
780
+ type: MessageType.Inline
749
781
  };
750
782
  break;
751
783
  case Events.RUN_STARTED:
752
784
  case Events.RUN_ENDED:
753
- message = {
754
- type: MessageType.Inline,
755
- text: renderRunEvent(event as RunEvent)
785
+ event._rendered = {
786
+ html: renderRunEvent(event as RunEvent),
787
+ type: MessageType.Inline
756
788
  };
757
789
  break;
758
790
  case Events.TICKET_ASSIGNEE_CHANGED:
759
- message = {
760
- type: MessageType.Inline,
761
- text: renderTicketAssigneeChanged(event as TicketEvent)
791
+ event._rendered = {
792
+ html: renderTicketAssigneeChanged(event as TicketEvent),
793
+ type: MessageType.Inline
762
794
  };
763
795
  break;
764
796
  case Events.TICKET_CLOSED:
765
- message = {
766
- type: MessageType.Inline,
767
- text: renderTicketAction(event as TicketEvent, 'closed')
797
+ event._rendered = {
798
+ html: renderTicketAction(event as TicketEvent, 'closed'),
799
+ type: MessageType.Inline
768
800
  };
769
801
  break;
770
802
  case Events.TICKET_OPENED:
771
- message = {
772
- type: MessageType.Inline,
773
- text: renderTicketAction(event as TicketEvent, 'opened')
803
+ event._rendered = {
804
+ html: renderTicketAction(event as TicketEvent, 'opened'),
805
+ type: MessageType.Inline
806
+ };
807
+ break;
808
+ case Events.TICKET_NOTE_ADDED:
809
+ event._rendered = {
810
+ html: renderTicketAction(event as TicketEvent, 'noted'),
811
+ type: MessageType.Inline
774
812
  };
775
813
  break;
776
814
  case Events.TICKET_REOPENED:
777
- message = {
778
- type: MessageType.Inline,
779
- text: renderTicketAction(event as TicketEvent, 'reopened')
815
+ event._rendered = {
816
+ html: renderTicketAction(event as TicketEvent, 'reopened'),
817
+ type: MessageType.Inline
780
818
  };
781
819
  break;
782
820
  case Events.TICKET_TOPIC_CHANGED:
783
- message = {
784
- type: MessageType.Inline,
785
- text: html`<div>
821
+ event._rendered = {
822
+ html: html`<div>
786
823
  Topic changed to
787
824
  <strong>${(event as TicketEvent).topic.name}</strong>
788
- </div>`
825
+ </div>`,
826
+ type: MessageType.Inline
789
827
  };
790
828
  break;
791
829
  case Events.CHANNEL_EVENT: // deprecated
792
- message = {
793
- type: MessageType.Inline,
794
- text: renderChannelEvent(event as ChannelEvent)
830
+ event._rendered = {
831
+ html: renderChannelEvent(event as ChannelEvent),
832
+ type: MessageType.Inline
795
833
  };
796
834
  break;
797
835
  default:
798
836
  console.error('Unknown event type', event);
799
837
  }
800
-
801
- if (message) {
802
- message.id = event.uuid;
803
- message.date = new Date(event.created_on);
804
- }
805
-
806
- return message;
807
- }
808
-
809
- private getUserForEvent(event: MsgEvent | TicketEvent) {
810
- if (event.type === 'msg_received') {
811
- return {
812
- name: this.currentContact.name
813
- };
814
- } else if (event._user) {
815
- return event._user;
816
- }
817
- return null;
818
838
  }
819
839
 
820
- private createChatForMessageEvent(msgEvent: MsgEvent): any {
821
- return {
822
- id: msgEvent.uuid,
823
- type: msgEvent.type === 'msg_received' ? 'msg_in' : 'msg_out',
824
- user: this.getUserForEvent(msgEvent),
825
- date: new Date(msgEvent.created_on),
826
- attachments: msgEvent.msg.attachments,
827
- text: msgEvent.msg.text,
828
- sendError:
829
- msgEvent._status &&
830
- (msgEvent._status.status === 'errored' ||
831
- msgEvent._status.status === 'failed'),
832
- popup: html`<div
833
- style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
834
- >
835
- <div style="justify-content:left;text-align:left">
836
- <temba-date
837
- value=${msgEvent.created_on}
838
- display="duration"
839
- ></temba-date>
840
-
841
- ${msgEvent.optin
842
- ? html`<div style="font-size:0.9em;color:#aaa">
843
- ${msgEvent.optin.name}
844
- </div>`
845
- : null}
846
- </div>
847
- ${msgEvent._logs_url
848
- ? html`<a style="margin-left:0.5em" href="${msgEvent._logs_url}"
849
- ><temba-icon name="log"></temba-icon
850
- ></a>`
851
- : null}
852
- </div> `
853
- };
854
- }
855
-
856
- private createMessages(page: ContactHistoryPage): ChatEvent[] {
840
+ private createMessages(page: ContactHistoryPage): ContactEvent[] {
857
841
  if (page.events) {
858
- let messages = [];
842
+ const messages: ContactEvent[] = [];
859
843
  page.events.forEach((event) => {
860
- const ts = new Date(event.created_on).getTime() * 1000;
861
- if (ts > this.newestEventTime) {
862
- this.newestEventTime = ts;
844
+ // track the UUID of the newest event for polling
845
+ if (
846
+ !this.afterUUID ||
847
+ event.uuid.toLowerCase() > this.afterUUID.toLowerCase()
848
+ ) {
849
+ this.afterUUID = event.uuid;
863
850
  }
864
851
 
865
- if (event.type === 'ticket_note_added') {
866
- const ticketEvent = event as TicketEvent;
867
- messages.push({
868
- type: MessageType.Note,
869
- id: event.created_on + event.type,
870
- user: this.getUserForEvent(ticketEvent),
871
- date: new Date(ticketEvent.created_on),
872
- text: ticketEvent.note
873
- });
874
- } else if (event.type === 'ticket_opened') {
875
- // ticket open events can have a note attached
876
- const ticketEvent = event as TicketEvent;
877
- messages.push({
878
- type: MessageType.Note,
879
- id: event.created_on + event.type + '_note',
880
- user: this.getUserForEvent(ticketEvent),
881
- date: new Date(ticketEvent.created_on),
882
- text: ticketEvent.note
883
- });
852
+ // convert to dates
853
+ event.created_on = new Date(event.created_on);
884
854
 
885
- // but the opening of the ticket is a normal event
886
- messages.push(this.getEventMessage(event));
887
- } else if (
855
+ if (
888
856
  event.type === 'msg_created' ||
889
857
  event.type === 'msg_received' ||
890
858
  event.type === 'ivr_created'
891
859
  ) {
892
- const msgEvent = event as MsgEvent;
893
- messages.push(this.createChatForMessageEvent(msgEvent));
860
+ messages.push(event);
894
861
  } else {
895
- const msg = this.getEventMessage(event);
896
- if (msg) {
897
- messages.push(msg);
862
+ this.prerender(event);
863
+ if (event._rendered) {
864
+ messages.push(event);
898
865
  }
899
866
  }
900
867
  });
901
868
 
902
869
  // remove any messages we don't recognize
903
- messages = messages.filter((msg) => !!msg);
904
- return messages as ChatEvent[];
870
+ return messages.filter((msg) => !!msg);
905
871
  }
906
872
  return [];
907
873
  }
@@ -913,9 +879,9 @@ export class ContactChat extends ContactStoreElement {
913
879
  }
914
880
 
915
881
  const chat = this.chat;
916
- const contactChat = this;
917
- if (this.currentContact && this.newestEventTime) {
882
+ if (this.currentContact && this.afterUUID) {
918
883
  this.polling = true;
884
+ this.lastFetchTime = Date.now();
919
885
  const endpoint = this.getEndpoint();
920
886
  if (!endpoint) {
921
887
  return;
@@ -924,23 +890,21 @@ export class ContactChat extends ContactStoreElement {
924
890
  const fetchContact = this.currentContact.uuid;
925
891
 
926
892
  fetchContactHistory(
927
- false,
928
893
  endpoint,
929
894
  this.currentTicket?.uuid,
930
895
  null,
931
- this.newestEventTime
896
+ this.afterUUID
932
897
  ).then((page: ContactHistoryPage) => {
898
+ const messages = this.createMessages(page);
899
+ messages.reverse();
933
900
  if (fetchContact === this.currentContact.uuid) {
934
- this.lastEventTime = page.next_before;
935
- const messages = this.createMessages(page);
936
- if (messages.length === 0) {
937
- contactChat.blockFetching = true;
938
- }
939
- messages.reverse();
901
+ const hasNewEvents = messages.length > 0;
940
902
  chat.addMessages(messages, null, true);
903
+ this.polling = false;
904
+ this.scheduleRefresh(hasNewEvents);
905
+ } else {
906
+ this.polling = false;
941
907
  }
942
- this.polling = false;
943
- this.scheduleRefresh();
944
908
  });
945
909
  }
946
910
  }
@@ -959,19 +923,37 @@ export class ContactChat extends ContactStoreElement {
959
923
  return;
960
924
  }
961
925
 
926
+ // initialize anchor UUID if not set (first fetch)
927
+ if (!this.beforeUUID && !this.afterUUID) {
928
+ // generate a UUID v7 for current time as the anchor
929
+ const anchorUUID = generateUUIDv7();
930
+ this.beforeUUID = anchorUUID;
931
+ this.afterUUID = anchorUUID;
932
+ }
933
+
962
934
  fetchContactHistory(
963
- false,
964
935
  endpoint,
965
936
  this.currentTicket?.uuid,
966
- this.lastEventTime
937
+ this.beforeUUID,
938
+ null
967
939
  ).then((page: ContactHistoryPage) => {
968
- this.lastEventTime = page.next_before;
969
940
  const messages = this.createMessages(page);
970
941
  messages.reverse();
971
942
 
972
943
  if (messages.length === 0) {
973
944
  contactChat.blockFetching = true;
945
+ } else if (page.next) {
946
+ // update beforeUUID for next fetch of older messages
947
+ this.beforeUUID = page.next;
948
+ } else {
949
+ // no more history, mark end and show oldest event date
950
+ contactChat.blockFetching = true;
951
+ if (page.events && page.events.length > 0) {
952
+ const oldestEvent = page.events[page.events.length - 1];
953
+ chat.setEndOfHistory(new Date(oldestEvent.created_on));
954
+ }
974
955
  }
956
+
975
957
  chat.addMessages(messages);
976
958
  this.scheduleRefresh();
977
959
  });
@@ -1087,15 +1069,13 @@ export class ContactChat extends ContactStoreElement {
1087
1069
  if (this.currentTicket) {
1088
1070
  fetchResults(`/api/v2/tickets.json?uuid=${this.currentTicket.uuid}`).then(
1089
1071
  (values) => {
1090
- this.store.resolveUsers(values, ['assignee']).then(() => {
1091
- if (values.length > 0) {
1092
- this.fireCustomEvent(CustomEventType.TicketUpdated, {
1093
- ticket: values[0],
1094
- previous: this.currentTicket
1095
- });
1096
- this.currentTicket = values[0];
1097
- }
1098
- });
1072
+ if (values.length > 0) {
1073
+ this.fireCustomEvent(CustomEventType.TicketUpdated, {
1074
+ ticket: values[0],
1075
+ previous: this.currentTicket
1076
+ });
1077
+ this.currentTicket = values[0];
1078
+ }
1099
1079
  }
1100
1080
  );
1101
1081
  }
@@ -1142,6 +1122,7 @@ export class ContactChat extends ContactStoreElement {
1142
1122
  @temba-fetch-complete=${this.fetchComplete}
1143
1123
  avatar=${this.avatar}
1144
1124
  agent
1125
+ ?hasFooter=${inFlow}
1145
1126
  >
1146
1127
  ${inFlow
1147
1128
  ? html`
@@ -1238,37 +1219,33 @@ export const fetchContact = (endpoint: string): Promise<Contact> => {
1238
1219
  });
1239
1220
  };
1240
1221
  export const fetchContactHistory = (
1241
- reset: boolean,
1242
1222
  endpoint: string,
1243
- ticket: string,
1244
- before: number = undefined,
1245
- after: number = undefined
1223
+ ticket: string = undefined,
1224
+ before: string = undefined,
1225
+ after: string = undefined
1246
1226
  ): Promise<ContactHistoryPage> => {
1247
- if (reset) {
1248
- pendingRequests.forEach((controller) => {
1249
- controller.abort();
1250
- });
1251
- pendingRequests = [];
1252
- }
1253
-
1254
1227
  return new Promise<ContactHistoryPage>((resolve) => {
1255
1228
  const controller = new AbortController();
1256
1229
  pendingRequests.push(controller);
1257
1230
 
1258
1231
  let url = endpoint;
1232
+ const params = [];
1233
+
1259
1234
  if (before) {
1260
- url += `&before=${before}`;
1235
+ params.push(`before=${before}`);
1261
1236
  }
1262
1237
 
1263
1238
  if (after) {
1264
- url += `&after=${after}`;
1239
+ params.push(`after=${after}`);
1265
1240
  }
1266
1241
 
1267
1242
  if (ticket) {
1268
- url += `&ticket=${ticket}`;
1243
+ params.push(`ticket=${ticket}`);
1269
1244
  }
1270
1245
 
1271
- const store = document.querySelector('temba-store') as Store;
1246
+ if (params.length > 0) {
1247
+ url += (url.includes('?') ? '&' : '?') + params.join('&');
1248
+ }
1272
1249
 
1273
1250
  getUrl(url, controller)
1274
1251
  .then((response: WebResponse) => {
@@ -1279,10 +1256,7 @@ export const fetchContactHistory = (
1279
1256
  }
1280
1257
  );
1281
1258
 
1282
- const page = response.json as ContactHistoryPage;
1283
- store.resolveUsers(page.events, ['created_by']).then(() => {
1284
- resolve(page);
1285
- });
1259
+ resolve(response.json as ContactHistoryPage);
1286
1260
  })
1287
1261
  .catch(() => {
1288
1262
  // canceled