@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
@@ -1,41 +1,102 @@
1
1
  import { TemplateResult, html, PropertyValueMap, css } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
+ import { repeat } from 'lit/directives/repeat.js';
3
4
  import { RapidElement } from '../RapidElement';
4
5
  import { CustomEventType } from '../interfaces';
5
6
  import { DEFAULT_AVATAR } from '../webchat/assets';
6
- import { hashCode } from '../utils';
7
7
 
8
8
  const BATCH_TIME_WINDOW = 60 * 60 * 1000;
9
- const SCROLL_FETCH_BUFFER = 0.05;
9
+ const SCROLL_FETCH_BUFFER = 200; // pixels from top
10
10
  const MIN_FETCH_TIME = 250;
11
11
 
12
+ const getUnsendableReasonMessage = (reason: string): string => {
13
+ switch (reason) {
14
+ case 'no_route':
15
+ return 'No channel available to send message';
16
+ case 'contact_blocked':
17
+ return 'Contact has been blocked';
18
+ case 'contact_stopped':
19
+ return 'Contact has been stopped';
20
+ case 'contact_archived':
21
+ return 'Contact is archived';
22
+ case 'org_suspended':
23
+ return 'Workspace is suspended';
24
+ case 'looping':
25
+ return 'Message loop detected';
26
+ default:
27
+ return 'Unable to send message';
28
+ }
29
+ };
30
+
31
+ const getStatusReasonMessage = (reason: string): string => {
32
+ switch (reason) {
33
+ case 'error_limit':
34
+ return 'Error limit reached';
35
+ case 'too_old':
36
+ return 'Message is too old to send';
37
+ case 'channel_removed':
38
+ return 'Channel was removed';
39
+ default:
40
+ return 'Message failed to send';
41
+ }
42
+ };
43
+
12
44
  export enum MessageType {
13
45
  Inline = 'inline',
14
46
  Error = 'error',
15
47
  Collapse = 'collapse',
16
- Note = 'note',
17
- MsgIn = 'msg_in',
18
- MsgOut = 'msg_out'
48
+ Note = 'note'
49
+ }
50
+
51
+ export interface ObjectReference {
52
+ uuid: string;
53
+ name: string;
19
54
  }
20
55
 
21
- interface User {
56
+ interface User extends ObjectReference {
22
57
  avatar?: string;
23
58
  email: string;
24
- name: string;
25
59
  }
26
60
 
27
- export interface ChatEvent {
28
- id?: string;
29
- type: MessageType;
30
- text: TemplateResult;
31
- date: Date;
32
- user?: User;
33
- popup?: TemplateResult;
61
+ export interface Msg {
62
+ text: string;
63
+ channel: ObjectReference;
64
+ quick_replies: string[];
65
+ urn: string;
66
+ direction: string;
67
+ type: string;
68
+ attachments: string[];
69
+ unsendable_reason?:
70
+ | 'no_route'
71
+ | 'contact_blocked'
72
+ | 'contact_stopped'
73
+ | 'contact_archived'
74
+ | 'org_suspended'
75
+ | 'looping';
34
76
  }
35
77
 
36
- export interface Message extends ChatEvent {
37
- sendError?: boolean;
38
- attachments?: string[];
78
+ export interface ContactEvent {
79
+ uuid?: string;
80
+ type: string;
81
+ created_on: Date;
82
+ _user?: User;
83
+ _rendered?: { html: TemplateResult; type: MessageType };
84
+ }
85
+
86
+ export interface MsgEvent extends ContactEvent {
87
+ msg: Msg;
88
+ optin?: ObjectReference;
89
+ _status?: {
90
+ created_on: string;
91
+ status: 'wired' | 'sent' | 'delivered' | 'read' | 'errored' | 'failed';
92
+ reason: 'error_limit' | 'too_old' | 'channel_removed';
93
+ };
94
+ _deleted?: {
95
+ created_on: string;
96
+ by_contact: boolean;
97
+ user: { name: string; uuid: string };
98
+ };
99
+ _logs_url?: string;
39
100
  }
40
101
 
41
102
  const TIME_FORMAT = { hour: 'numeric', minute: '2-digit' } as any;
@@ -104,7 +165,8 @@ export class Chat extends RapidElement {
104
165
  text-align: center;
105
166
  font-size: 0.8em;
106
167
  color: #999;
107
- margin-top: 2em;
168
+ margin-bottom: 2em;
169
+ margin-top: 1em;
108
170
  border-top: 1px solid #e9e9e9;
109
171
  padding: 1em;
110
172
  margin-left: 10%;
@@ -216,16 +278,24 @@ export class Chat extends RapidElement {
216
278
  color: rgba(0, 0, 0, 0.5);
217
279
  }
218
280
 
281
+ .failed .bubble,
282
+ .error .bubble {
283
+ border: 1px solid var(--color-error);
284
+ background: #ffe6e6;
285
+ color: #ad4747ff;
286
+ }
287
+
288
+ .error .bubble .name,
289
+ .failed .bubble .name {
290
+ color: #ad47479a;
291
+ }
292
+
219
293
  .message {
220
294
  margin-bottom: 0.5em;
221
295
  line-height: 1.2em;
222
296
  word-break: break-word;
223
297
  }
224
298
 
225
- .message-text {
226
- white-space: pre-line;
227
- }
228
-
229
299
  .chat {
230
300
  width: 28rem;
231
301
  border-radius: var(--curvature);
@@ -436,19 +506,12 @@ export class Chat extends RapidElement {
436
506
  border-radius: var(--curvature);
437
507
  }
438
508
 
439
- .error .bubble {
440
- border: 1px solid var(--color-error);
441
- background: white;
442
- color: #333;
443
- }
444
-
445
- .error .bubble .name {
446
- color: #999;
447
- }
448
-
509
+ .failed temba-thumbnail,
449
510
  .error temba-thumbnail {
450
- --thumb-background: var(--color-error);
451
- --thumb-icon: white;
511
+ --thumb-background: #ffe6e6;
512
+ --thumb-border: var(--color-error);
513
+ border: 1px solid var(--color-error);
514
+ color: #ad4747a8;
452
515
  }
453
516
 
454
517
  .outgoing .popup {
@@ -492,6 +555,37 @@ export class Chat extends RapidElement {
492
555
  opacity: 1;
493
556
  transition-delay: 1s;
494
557
  }
558
+
559
+ .new-message-notification {
560
+ position: absolute;
561
+ bottom: 1em;
562
+ left: 50%;
563
+ transform: translateX(-50%) translateY(100px);
564
+ background: var(--color-primary-dark, #3c92dd);
565
+ color: white;
566
+ padding: 0.75em 1.5em;
567
+ border-radius: var(--curvature);
568
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 7px 0px,
569
+ rgba(0, 0, 0, 0.3) 0px 1px 2px 0px;
570
+ cursor: pointer;
571
+ opacity: 0;
572
+ transition: all 0.3s ease-out;
573
+ z-index: 100;
574
+ font-weight: 500;
575
+ pointer-events: none;
576
+ }
577
+
578
+ .new-message-notification.visible {
579
+ transform: translateX(-50%) translateY(0);
580
+ opacity: 1;
581
+ pointer-events: auto;
582
+ }
583
+
584
+ .new-message-notification:hover {
585
+ background: var(--color-primary-darker, #2b7ac4);
586
+ box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 10px 0px,
587
+ rgba(0, 0, 0, 0.4) 0px 2px 4px 0px;
588
+ }
495
589
  `;
496
590
  }
497
591
 
@@ -513,7 +607,20 @@ export class Chat extends RapidElement {
513
607
  @property({ type: Boolean })
514
608
  agent = false;
515
609
 
516
- private msgMap = new Map<string, ChatEvent>();
610
+ @property({ type: Boolean, attribute: false })
611
+ endOfHistory = false;
612
+
613
+ @property({ type: Object, attribute: false })
614
+ oldestEventDate: Date = null;
615
+
616
+ @property({ type: Boolean, attribute: false })
617
+ showNewMessageNotification = false;
618
+
619
+ @property({ type: Boolean })
620
+ hasFooter = false;
621
+
622
+ private msgMap = new Map<string, ContactEvent>();
623
+ private metadataCache = new Map<string, ContactEvent>();
517
624
 
518
625
  public firstUpdated(
519
626
  changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
@@ -526,20 +633,10 @@ export class Chat extends RapidElement {
526
633
  }
527
634
 
528
635
  public addMessages(
529
- messages: ChatEvent[],
636
+ messages: ContactEvent[],
530
637
  startTime: Date = null,
531
638
  append = false
532
639
  ) {
533
- // make sure our messages have ids
534
- messages.forEach((m) => {
535
- if (!m.id) {
536
- m.id =
537
- hashCode((m.text.strings || []).join('')) +
538
- '_' +
539
- m.date.toISOString();
540
- }
541
- });
542
-
543
640
  if (!startTime) {
544
641
  startTime = new Date();
545
642
  }
@@ -551,8 +648,17 @@ export class Chat extends RapidElement {
551
648
  // first add messages to the map
552
649
  const newMessages = [];
553
650
  for (const m of messages) {
651
+ // filter out metadata events - they aren't rendered but cached for later reference
652
+ if (m.type === 'msg_deleted' || m.type === 'msg_status_changed') {
653
+ const msgUuid = (m as any).msg_uuid;
654
+ if (msgUuid) {
655
+ this.metadataCache.set(msgUuid, m);
656
+ }
657
+ continue;
658
+ }
659
+
554
660
  if (this.addMessage(m)) {
555
- newMessages.push(m.id);
661
+ newMessages.push(m.uuid);
556
662
  }
557
663
  }
558
664
 
@@ -562,12 +668,28 @@ export class Chat extends RapidElement {
562
668
 
563
669
  const ele = this.shadowRoot.querySelector('.scroll');
564
670
  const prevTop = ele.scrollTop;
671
+ const prevScrollHeight = ele.scrollHeight;
672
+ const scrollableHeight = ele.scrollHeight - ele.clientHeight;
673
+ const isScrolledAway =
674
+ scrollableHeight > 0 && Math.abs(ele.scrollTop) > 50;
565
675
 
566
676
  const grouped = this.groupMessages(newMessages);
567
677
  this.insertGroups(grouped, append);
568
678
 
679
+ // show notification if new messages are appended and user is scrolled away from bottom
680
+ if (append && isScrolledAway && newMessages.length > 0) {
681
+ this.showNewMessageNotification = true;
682
+ }
683
+
569
684
  window.setTimeout(() => {
570
- ele.scrollTop = prevTop;
685
+ // when appending (new messages at bottom), adjust scroll to maintain visible content
686
+ // with column-reverse, new content at bottom increases scrollHeight
687
+ if (append && isScrolledAway) {
688
+ const heightDiff = ele.scrollHeight - prevScrollHeight;
689
+ ele.scrollTop = prevTop - heightDiff;
690
+ } else {
691
+ ele.scrollTop = prevTop;
692
+ }
571
693
 
572
694
  this.fireCustomEvent(CustomEventType.FetchComplete);
573
695
  }, 100);
@@ -579,23 +701,24 @@ export class Chat extends RapidElement {
579
701
  );
580
702
  }
581
703
 
582
- private addMessage(msg: ChatEvent): boolean {
704
+ private addMessage(msg: ContactEvent): boolean {
583
705
  const isNew = !this.messageExists(msg);
584
- this.msgMap.set(msg.id, msg);
706
+ this.msgMap.set(msg.uuid, msg);
585
707
  return isNew;
586
708
  }
587
709
 
588
- public messageExists(msg: ChatEvent): boolean {
589
- return this.msgMap.has(msg.id);
710
+ public messageExists(msg: ContactEvent): boolean {
711
+ return this.msgMap.has(msg.uuid);
590
712
  }
591
713
 
592
- private isSameGroup(msg1: ChatEvent, msg2: ChatEvent): boolean {
714
+ private isSameGroup(msg1: ContactEvent, msg2: ContactEvent): boolean {
593
715
  if (msg1 && msg2) {
594
- return (
716
+ const sameGroup =
595
717
  msg1.type === msg2.type &&
596
- msg1.user?.name === msg2.user?.name &&
597
- Math.abs(msg1.date.getTime() - msg2.date.getTime()) < BATCH_TIME_WINDOW
598
- );
718
+ msg1._user?.name === msg2._user?.name &&
719
+ Math.abs(msg1.created_on.getTime() - msg2.created_on.getTime()) <
720
+ BATCH_TIME_WINDOW;
721
+ return sameGroup;
599
722
  }
600
723
 
601
724
  return false;
@@ -657,14 +780,29 @@ export class Chat extends RapidElement {
657
780
 
658
781
  private handleScroll(event: any) {
659
782
  const ele = event.target;
660
- const top = ele.scrollHeight - ele.clientHeight;
661
- const scroll = Math.round(top + ele.scrollTop);
662
- const scrollPct = scroll / top;
783
+ const scrollableHeight = ele.scrollHeight - ele.clientHeight;
784
+
785
+ if (scrollableHeight <= 0) {
786
+ return;
787
+ }
788
+
789
+ // with column-reverse, scrollTop behavior depends on the browser
790
+ // check if scrollTop is negative (some browsers) or positive (others)
791
+ const absScrollTop = Math.abs(ele.scrollTop);
792
+
793
+ // when scrolling up to older messages, absScrollTop increases
794
+ // trigger when we're close to the maximum scroll (oldest messages)
795
+ const shouldFetch = absScrollTop >= scrollableHeight - SCROLL_FETCH_BUFFER;
663
796
 
664
- this.hideTopScroll = scrollPct <= 0.01;
665
- this.hideBottomScroll = scrollPct >= 0.99;
797
+ this.hideTopScroll = absScrollTop >= scrollableHeight - 1;
798
+ this.hideBottomScroll = absScrollTop <= 1;
666
799
 
667
- if (scrollPct < SCROLL_FETCH_BUFFER) {
800
+ // hide notification when scrolled to bottom
801
+ if (absScrollTop <= 10) {
802
+ this.showNewMessageNotification = false;
803
+ }
804
+
805
+ if (shouldFetch) {
668
806
  this.fireCustomEvent(CustomEventType.ScrollThreshold);
669
807
  }
670
808
  }
@@ -672,11 +810,16 @@ export class Chat extends RapidElement {
672
810
  private scrollToBottom() {
673
811
  const scroll = this.shadowRoot.querySelector('.scroll');
674
812
  if (scroll) {
675
- scroll.scrollTop = scroll.scrollHeight;
813
+ scroll.scrollTop = 0;
676
814
  this.hideBottomScroll = true;
815
+ this.showNewMessageNotification = false;
677
816
  }
678
817
  }
679
818
 
819
+ private handleNewMessageClick() {
820
+ this.scrollToBottom();
821
+ }
822
+
680
823
  private renderMessageGroup(
681
824
  msgIds: string[],
682
825
  idx: number,
@@ -685,7 +828,7 @@ export class Chat extends RapidElement {
685
828
  const today = new Date();
686
829
  const firstGroup = idx === groups.length - 1;
687
830
 
688
- let prevMsg: ChatEvent;
831
+ let prevMsg: ContactEvent;
689
832
  if (idx > 0) {
690
833
  const lastGroup = groups[idx - 1];
691
834
  if (lastGroup && lastGroup.length > 0) {
@@ -695,116 +838,137 @@ export class Chat extends RapidElement {
695
838
 
696
839
  const mostRecentId = msgIds[msgIds.length - 1];
697
840
  const currentMsg = this.msgMap.get(mostRecentId);
841
+
698
842
  let timeDisplay = null;
699
843
  if (
700
844
  prevMsg &&
701
845
  !this.isSameGroup(prevMsg, currentMsg) &&
702
- (Math.abs(currentMsg.date.getTime() - prevMsg.date.getTime()) >
703
- BATCH_TIME_WINDOW ||
846
+ (Math.abs(
847
+ currentMsg.created_on.getTime() - prevMsg.created_on.getTime()
848
+ ) > BATCH_TIME_WINDOW ||
704
849
  idx === groups.length - 1)
705
850
  ) {
706
851
  if (
707
- today.getDate() !== prevMsg.date.getDate() ||
708
- prevMsg.date.getDate() !== currentMsg.date.getDate()
852
+ today.getDate() !== prevMsg.created_on.getDate() ||
853
+ prevMsg.created_on.getDate() !== currentMsg.created_on.getDate()
709
854
  ) {
710
855
  timeDisplay = html`<div class="time ${firstGroup ? 'first' : ''}">
711
- ${prevMsg.date.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
856
+ ${prevMsg.created_on.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
712
857
  </div>`;
713
858
  } else {
714
859
  timeDisplay = html`<div class="time ${firstGroup ? 'first' : ''}">
715
- ${prevMsg.date.toLocaleTimeString(undefined, TIME_FORMAT)}
860
+ ${prevMsg.created_on.toLocaleTimeString(undefined, TIME_FORMAT)}
716
861
  </div>`;
717
862
  }
718
863
  }
719
864
 
720
865
  const incoming = this.agent
721
- ? currentMsg.type !== 'msg_in'
722
- : currentMsg.type === 'msg_in';
866
+ ? currentMsg.type !== 'msg_received'
867
+ : currentMsg.type === 'msg_received';
723
868
 
724
- const name = currentMsg.user?.name;
725
- const email = currentMsg.user?.email;
869
+ const name = currentMsg._user?.name;
726
870
 
727
871
  const showAvatar =
728
- ((currentMsg.type === 'note' ||
729
- currentMsg.type === 'msg_in' ||
730
- currentMsg.type === 'msg_out') &&
872
+ ((currentMsg.type === 'msg_received' ||
873
+ currentMsg.type === 'msg_created') &&
731
874
  this.agent) ||
732
875
  !incoming;
733
876
 
877
+ const isSystem = !currentMsg._user?.uuid;
878
+
734
879
  return html`
735
- ${!firstGroup ? timeDisplay : null}
736
- <div
737
- class="block ${incoming ? 'incoming' : 'outgoing'} ${currentMsg.type}"
738
- >
880
+ ${timeDisplay}
881
+ <div class="block ${incoming ? 'incoming' : 'outgoing'}">
739
882
  <div class="group-messages" style="flex-grow:1">
740
- ${msgIds.map((msgId, index) => {
741
- const msg = this.msgMap.get(msgId);
742
- return html`<div class="row message">
743
- ${this.renderMessage(msg, index == 0 ? name : null)}
744
- </div>`;
745
- })}
883
+ ${repeat(
884
+ msgIds,
885
+ (msgId) => msgId,
886
+ (msgId, index) => {
887
+ const msg = this.msgMap.get(msgId);
888
+ const msgEvent = msg as MsgEvent;
889
+ const statusClass = (msg as any)._status
890
+ ? (msg as any)._status.status
891
+ : '';
892
+ const hasError =
893
+ msgEvent.msg?.unsendable_reason ||
894
+ (msgEvent._status?.reason &&
895
+ (statusClass === 'failed' || statusClass === 'errored'));
896
+ const unsendableClass = hasError ? 'error' : '';
897
+ return html`<div
898
+ class="row message ${statusClass} ${unsendableClass}"
899
+ >
900
+ ${this.renderMessage(msg, index == 0 ? name : null)}
901
+ </div>`;
902
+ }
903
+ )}
746
904
  </div>
747
905
  ${showAvatar
748
906
  ? html`<div class="avatar" style="align-self:flex-end">
749
907
  <temba-user
750
- email=${email}
908
+ uuid=${currentMsg._user?.uuid}
751
909
  name=${name}
752
- avatar=${currentMsg.user?.avatar}
753
- ?system=${!email && !name}
910
+ avatar=${currentMsg._user?.avatar}
911
+ ?system=${isSystem}
754
912
  >
755
913
  </temba-user>
756
914
  </div>`
757
915
  : null}
758
916
  </div>
759
- ${firstGroup ? timeDisplay : null}
760
917
  `;
761
918
  }
762
919
 
763
- private renderMessage(event: ChatEvent, name = null): TemplateResult {
764
- if (
765
- event.type === MessageType.Error ||
766
- event.type === MessageType.Collapse ||
767
- event.type === MessageType.Inline
768
- ) {
769
- return html`<div class="event">${event.text}</div>`;
920
+ private renderMessage(event: ContactEvent, name = null): TemplateResult {
921
+ if (event._rendered) {
922
+ return html`<div class="event">${event._rendered.html}</div>`;
770
923
  }
771
924
 
772
- const message = event as Message;
925
+ const message = event as MsgEvent;
926
+ const unsendableReason = message.msg?.unsendable_reason;
927
+ const statusReason = message._status?.reason;
928
+ const errorMessage = unsendableReason
929
+ ? getUnsendableReasonMessage(unsendableReason)
930
+ : statusReason
931
+ ? getStatusReasonMessage(statusReason)
932
+ : null;
933
+
773
934
  return html`
774
- <div class="bubble-wrap ${message.sendError ? 'error' : ''}">
775
- ${
776
- message.popup
777
- ? html`<div class="popup">
778
- ${message.popup}
779
- <div class="arrow">▼</div>
935
+ <div class="bubble-wrap">
936
+ <div class="popup" style="white-space: nowrap;">
937
+ ${errorMessage
938
+ ? html`<div style="color: var(--color-error); margin-right: 1em;">
939
+ ${errorMessage}
780
940
  </div>`
781
- : null
782
- }
783
-
784
- ${
785
- message.text
786
- ? html`
787
- <div class="bubble">
788
- ${name ? html`<div class="name">${name}</div>` : null}
789
- <div class="message message-text">${message.text}</div>
790
-
791
- <!--div>${message.date.toLocaleDateString(
792
- undefined,
793
- VERBOSE_FORMAT
794
- )}</div-->
795
- </div>
796
- `
797
- : null
798
- }
941
+ : null}
942
+ <temba-date
943
+ value="${message.created_on.toISOString()}"
944
+ display="relative"
945
+ ></temba-date>
946
+ ${message._logs_url
947
+ ? html`<a
948
+ style="margin-left: 1em; color: var(--color-primary-dark);"
949
+ href="${message._logs_url}"
950
+ target="_blank"
951
+ rel="noopener noreferrer"
952
+ ><temba-icon name="log"></temba-icon
953
+ ></a>`
954
+ : null}
955
+
956
+ <div class="arrow">▼</div>
957
+ </div>
958
+ ${message.msg.text
959
+ ? html`<div class="bubble">
960
+ ${name ? html`<div class="name">${name}</div>` : null}
961
+ <div class="message message-text">${message.msg.text}</div>
962
+ </div>`
963
+ : null}
799
964
 
800
- <div class="attachments">
801
- ${(message.attachments || []).map(
802
- (attachment) =>
803
- html`<temba-thumbnail
804
- attachment="${attachment}"
805
- ></temba-thumbnail>`
806
- )}
807
- </div>
965
+ <div class="attachments">
966
+ ${(message.msg.attachments || []).map(
967
+ (attachment) =>
968
+ html`<temba-thumbnail
969
+ attachment="${attachment}"
970
+ ></temba-thumbnail>`
971
+ )}
808
972
  </div>
809
973
  </div>
810
974
  `;
@@ -815,6 +979,13 @@ export class Chat extends RapidElement {
815
979
  this.messageGroups = [];
816
980
  this.hideBottomScroll = true;
817
981
  this.hideTopScroll = true;
982
+ this.endOfHistory = false;
983
+ this.oldestEventDate = null;
984
+ }
985
+
986
+ public setEndOfHistory(oldestDate: Date) {
987
+ this.endOfHistory = true;
988
+ this.oldestEventDate = oldestDate;
818
989
  }
819
990
 
820
991
  public render(): TemplateResult {
@@ -826,16 +997,41 @@ export class Chat extends RapidElement {
826
997
  >
827
998
  <div class="scroll" @scroll=${this.handleScroll}>
828
999
  ${this.messageGroups
829
- ? this.messageGroups.map(
830
- (msgGroup, idx, groups) =>
831
- html`${this.renderMessageGroup(msgGroup, idx, groups)}`
1000
+ ? repeat(
1001
+ this.messageGroups,
1002
+ (msgGroup) => msgGroup.join(','),
1003
+ (msgGroup, idx) =>
1004
+ html`${this.renderMessageGroup(
1005
+ msgGroup,
1006
+ idx,
1007
+ this.messageGroups
1008
+ )}`
832
1009
  )
833
1010
  : null}
834
1011
 
835
1012
  <temba-loading
836
1013
  class="${!this.fetching ? 'hidden' : ''}"
837
1014
  ></temba-loading>
1015
+
1016
+ ${this.endOfHistory && this.oldestEventDate
1017
+ ? html`<div class="time first">
1018
+ ${this.oldestEventDate.toLocaleTimeString(
1019
+ undefined,
1020
+ VERBOSE_FORMAT
1021
+ )}
1022
+ </div>`
1023
+ : null}
838
1024
  </div>
1025
+ ${!this.hasFooter
1026
+ ? html`<div
1027
+ class="new-message-notification ${this.showNewMessageNotification
1028
+ ? 'visible'
1029
+ : ''}"
1030
+ @click=${this.handleNewMessageClick}
1031
+ >
1032
+ New Messages
1033
+ </div>`
1034
+ : null}
839
1035
  <slot class="header" name="header"></slot>
840
1036
  <slot class="footer" name="footer"></slot>
841
1037
  </div>`;