@nyaruka/temba-components 0.133.0 → 0.134.1

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 (72) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/demo/components/webchat/example.html +1 -1
  3. package/dist/locales/es.js +5 -5
  4. package/dist/locales/es.js.map +1 -1
  5. package/dist/locales/fr.js +5 -5
  6. package/dist/locales/fr.js.map +1 -1
  7. package/dist/locales/locale-codes.js +2 -11
  8. package/dist/locales/locale-codes.js.map +1 -1
  9. package/dist/locales/pt.js +5 -5
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/temba-components.js +307 -259
  12. package/dist/temba-components.js.map +1 -1
  13. package/out-tsc/src/display/Chat.js +223 -90
  14. package/out-tsc/src/display/Chat.js.map +1 -1
  15. package/out-tsc/src/display/TembaUser.js +3 -3
  16. package/out-tsc/src/display/TembaUser.js.map +1 -1
  17. package/out-tsc/src/events.js.map +1 -1
  18. package/out-tsc/src/flow/CanvasNode.js +8 -0
  19. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  20. package/out-tsc/src/flow/Editor.js +117 -28
  21. package/out-tsc/src/flow/Editor.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +141 -0
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js.map +1 -1
  25. package/out-tsc/src/live/ContactChat.js +122 -170
  26. package/out-tsc/src/live/ContactChat.js.map +1 -1
  27. package/out-tsc/src/locales/es.js +5 -5
  28. package/out-tsc/src/locales/es.js.map +1 -1
  29. package/out-tsc/src/locales/fr.js +5 -5
  30. package/out-tsc/src/locales/fr.js.map +1 -1
  31. package/out-tsc/src/locales/locale-codes.js +2 -11
  32. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  33. package/out-tsc/src/locales/pt.js +5 -5
  34. package/out-tsc/src/locales/pt.js.map +1 -1
  35. package/out-tsc/src/store/AppState.js +3 -0
  36. package/out-tsc/src/store/AppState.js.map +1 -1
  37. package/out-tsc/src/store/Store.js +5 -5
  38. package/out-tsc/src/store/Store.js.map +1 -1
  39. package/out-tsc/src/webchat/WebChat.js +22 -9
  40. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  41. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  42. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  43. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  44. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  45. package/out-tsc/test/temba-flow-editor-node.test.js +128 -42
  46. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  47. package/package.json +1 -1
  48. package/screenshots/truth/contacts/chat-failure.png +0 -0
  49. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  50. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  51. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  52. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  53. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  54. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  55. package/src/display/Chat.ts +303 -129
  56. package/src/display/TembaUser.ts +3 -2
  57. package/src/events.ts +11 -8
  58. package/src/flow/CanvasNode.ts +10 -0
  59. package/src/flow/Editor.ts +156 -28
  60. package/src/flow/utils.ts +207 -1
  61. package/src/interfaces.ts +7 -0
  62. package/src/live/ContactChat.ts +129 -180
  63. package/src/locales/es.ts +13 -18
  64. package/src/locales/fr.ts +13 -18
  65. package/src/locales/locale-codes.ts +2 -11
  66. package/src/locales/pt.ts +13 -18
  67. package/src/store/AppState.ts +2 -0
  68. package/src/store/Store.ts +5 -5
  69. package/src/webchat/WebChat.ts +24 -10
  70. package/test/actions/send_broadcast.test.ts +2 -1
  71. package/test/temba-flow-collision.test.ts +833 -0
  72. package/test/temba-flow-editor-node.test.ts +142 -47
@@ -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'
19
49
  }
20
50
 
21
- interface User {
51
+ export interface ObjectReference {
52
+ uuid: string;
53
+ name: string;
54
+ }
55
+
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';
76
+ }
77
+
78
+ export interface ContactEvent {
79
+ uuid?: string;
80
+ type: string;
81
+ created_on: Date;
82
+ _user?: User;
83
+ _rendered?: { html: TemplateResult; type: MessageType };
34
84
  }
35
85
 
36
- export interface Message extends ChatEvent {
37
- sendError?: boolean;
38
- attachments?: string[];
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;
@@ -217,16 +278,24 @@ export class Chat extends RapidElement {
217
278
  color: rgba(0, 0, 0, 0.5);
218
279
  }
219
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
+
220
293
  .message {
221
294
  margin-bottom: 0.5em;
222
295
  line-height: 1.2em;
223
296
  word-break: break-word;
224
297
  }
225
298
 
226
- .message-text {
227
- white-space: pre-line;
228
- }
229
-
230
299
  .chat {
231
300
  width: 28rem;
232
301
  border-radius: var(--curvature);
@@ -437,19 +506,12 @@ export class Chat extends RapidElement {
437
506
  border-radius: var(--curvature);
438
507
  }
439
508
 
440
- .error .bubble {
441
- border: 1px solid var(--color-error);
442
- background: white;
443
- color: #333;
444
- }
445
-
446
- .error .bubble .name {
447
- color: #999;
448
- }
449
-
509
+ .failed temba-thumbnail,
450
510
  .error temba-thumbnail {
451
- --thumb-background: var(--color-error);
452
- --thumb-icon: white;
511
+ --thumb-background: #ffe6e6;
512
+ --thumb-border: var(--color-error);
513
+ border: 1px solid var(--color-error);
514
+ color: #ad4747a8;
453
515
  }
454
516
 
455
517
  .outgoing .popup {
@@ -493,6 +555,37 @@ export class Chat extends RapidElement {
493
555
  opacity: 1;
494
556
  transition-delay: 1s;
495
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
+ }
496
589
  `;
497
590
  }
498
591
 
@@ -520,7 +613,14 @@ export class Chat extends RapidElement {
520
613
  @property({ type: Object, attribute: false })
521
614
  oldestEventDate: Date = null;
522
615
 
523
- private msgMap = new Map<string, ChatEvent>();
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>();
524
624
 
525
625
  public firstUpdated(
526
626
  changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
@@ -533,20 +633,10 @@ export class Chat extends RapidElement {
533
633
  }
534
634
 
535
635
  public addMessages(
536
- messages: ChatEvent[],
636
+ messages: ContactEvent[],
537
637
  startTime: Date = null,
538
638
  append = false
539
639
  ) {
540
- // make sure our messages have ids
541
- messages.forEach((m) => {
542
- if (!m.id) {
543
- m.id =
544
- hashCode((m.text.strings || []).join('')) +
545
- '_' +
546
- m.date.toISOString();
547
- }
548
- });
549
-
550
640
  if (!startTime) {
551
641
  startTime = new Date();
552
642
  }
@@ -558,8 +648,17 @@ export class Chat extends RapidElement {
558
648
  // first add messages to the map
559
649
  const newMessages = [];
560
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
+
561
660
  if (this.addMessage(m)) {
562
- newMessages.push(m.id);
661
+ newMessages.push(m.uuid);
563
662
  }
564
663
  }
565
664
 
@@ -569,12 +668,28 @@ export class Chat extends RapidElement {
569
668
 
570
669
  const ele = this.shadowRoot.querySelector('.scroll');
571
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;
572
675
 
573
676
  const grouped = this.groupMessages(newMessages);
574
677
  this.insertGroups(grouped, append);
575
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
+
576
684
  window.setTimeout(() => {
577
- 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
+ }
578
693
 
579
694
  this.fireCustomEvent(CustomEventType.FetchComplete);
580
695
  }, 100);
@@ -586,22 +701,23 @@ export class Chat extends RapidElement {
586
701
  );
587
702
  }
588
703
 
589
- private addMessage(msg: ChatEvent): boolean {
704
+ private addMessage(msg: ContactEvent): boolean {
590
705
  const isNew = !this.messageExists(msg);
591
- this.msgMap.set(msg.id, msg);
706
+ this.msgMap.set(msg.uuid, msg);
592
707
  return isNew;
593
708
  }
594
709
 
595
- public messageExists(msg: ChatEvent): boolean {
596
- return this.msgMap.has(msg.id);
710
+ public messageExists(msg: ContactEvent): boolean {
711
+ return this.msgMap.has(msg.uuid);
597
712
  }
598
713
 
599
- private isSameGroup(msg1: ChatEvent, msg2: ChatEvent): boolean {
714
+ private isSameGroup(msg1: ContactEvent, msg2: ContactEvent): boolean {
600
715
  if (msg1 && msg2) {
601
716
  const sameGroup =
602
717
  msg1.type === msg2.type &&
603
- msg1.user?.name === msg2.user?.name &&
604
- Math.abs(msg1.date.getTime() - msg2.date.getTime()) < BATCH_TIME_WINDOW;
718
+ msg1._user?.name === msg2._user?.name &&
719
+ Math.abs(msg1.created_on.getTime() - msg2.created_on.getTime()) <
720
+ BATCH_TIME_WINDOW;
605
721
  return sameGroup;
606
722
  }
607
723
 
@@ -664,14 +780,29 @@ export class Chat extends RapidElement {
664
780
 
665
781
  private handleScroll(event: any) {
666
782
  const ele = event.target;
667
- const top = ele.scrollHeight - ele.clientHeight;
668
- const scroll = Math.round(top + ele.scrollTop);
669
- const scrollPct = scroll / top;
783
+ const scrollableHeight = ele.scrollHeight - ele.clientHeight;
670
784
 
671
- this.hideTopScroll = scrollPct <= 0.01;
672
- this.hideBottomScroll = scrollPct >= 0.99;
785
+ if (scrollableHeight <= 0) {
786
+ return;
787
+ }
673
788
 
674
- if (scrollPct < SCROLL_FETCH_BUFFER) {
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;
796
+
797
+ this.hideTopScroll = absScrollTop >= scrollableHeight - 1;
798
+ this.hideBottomScroll = absScrollTop <= 1;
799
+
800
+ // hide notification when scrolled to bottom
801
+ if (absScrollTop <= 10) {
802
+ this.showNewMessageNotification = false;
803
+ }
804
+
805
+ if (shouldFetch) {
675
806
  this.fireCustomEvent(CustomEventType.ScrollThreshold);
676
807
  }
677
808
  }
@@ -679,11 +810,16 @@ export class Chat extends RapidElement {
679
810
  private scrollToBottom() {
680
811
  const scroll = this.shadowRoot.querySelector('.scroll');
681
812
  if (scroll) {
682
- scroll.scrollTop = scroll.scrollHeight;
813
+ scroll.scrollTop = 0;
683
814
  this.hideBottomScroll = true;
815
+ this.showNewMessageNotification = false;
684
816
  }
685
817
  }
686
818
 
819
+ private handleNewMessageClick() {
820
+ this.scrollToBottom();
821
+ }
822
+
687
823
  private renderMessageGroup(
688
824
  msgIds: string[],
689
825
  idx: number,
@@ -692,7 +828,7 @@ export class Chat extends RapidElement {
692
828
  const today = new Date();
693
829
  const firstGroup = idx === groups.length - 1;
694
830
 
695
- let prevMsg: ChatEvent;
831
+ let prevMsg: ContactEvent;
696
832
  if (idx > 0) {
697
833
  const lastGroup = groups[idx - 1];
698
834
  if (lastGroup && lastGroup.length > 0) {
@@ -707,58 +843,72 @@ export class Chat extends RapidElement {
707
843
  if (
708
844
  prevMsg &&
709
845
  !this.isSameGroup(prevMsg, currentMsg) &&
710
- (Math.abs(currentMsg.date.getTime() - prevMsg.date.getTime()) >
711
- BATCH_TIME_WINDOW ||
846
+ (Math.abs(
847
+ currentMsg.created_on.getTime() - prevMsg.created_on.getTime()
848
+ ) > BATCH_TIME_WINDOW ||
712
849
  idx === groups.length - 1)
713
850
  ) {
714
851
  if (
715
- today.getDate() !== prevMsg.date.getDate() ||
716
- prevMsg.date.getDate() !== currentMsg.date.getDate()
852
+ today.getDate() !== prevMsg.created_on.getDate() ||
853
+ prevMsg.created_on.getDate() !== currentMsg.created_on.getDate()
717
854
  ) {
718
855
  timeDisplay = html`<div class="time ${firstGroup ? 'first' : ''}">
719
- ${prevMsg.date.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
856
+ ${prevMsg.created_on.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
720
857
  </div>`;
721
858
  } else {
722
859
  timeDisplay = html`<div class="time ${firstGroup ? 'first' : ''}">
723
- ${prevMsg.date.toLocaleTimeString(undefined, TIME_FORMAT)}
860
+ ${prevMsg.created_on.toLocaleTimeString(undefined, TIME_FORMAT)}
724
861
  </div>`;
725
862
  }
726
863
  }
727
864
 
728
865
  const incoming = this.agent
729
- ? currentMsg.type !== 'msg_in'
730
- : currentMsg.type === 'msg_in';
866
+ ? currentMsg.type !== 'msg_received'
867
+ : currentMsg.type === 'msg_received';
731
868
 
732
- const name = currentMsg.user?.name;
733
- const email = currentMsg.user?.email;
869
+ const name = currentMsg._user?.name;
734
870
 
735
871
  const showAvatar =
736
- ((currentMsg.type === 'note' ||
737
- currentMsg.type === 'msg_in' ||
738
- currentMsg.type === 'msg_out') &&
872
+ ((currentMsg.type === 'msg_received' ||
873
+ currentMsg.type === 'msg_created') &&
739
874
  this.agent) ||
740
875
  !incoming;
741
876
 
877
+ const isSystem = !currentMsg._user?.uuid;
878
+
742
879
  return html`
743
880
  ${timeDisplay}
744
- <div
745
- class="block ${incoming ? 'incoming' : 'outgoing'} ${currentMsg.type}"
746
- >
881
+ <div class="block ${incoming ? 'incoming' : 'outgoing'}">
747
882
  <div class="group-messages" style="flex-grow:1">
748
- ${msgIds.map((msgId, index) => {
749
- const msg = this.msgMap.get(msgId);
750
- return html`<div class="row message">
751
- ${this.renderMessage(msg, index == 0 ? name : null)}
752
- </div>`;
753
- })}
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
+ )}
754
904
  </div>
755
905
  ${showAvatar
756
906
  ? html`<div class="avatar" style="align-self:flex-end">
757
907
  <temba-user
758
- email=${email}
908
+ uuid=${currentMsg._user?.uuid}
759
909
  name=${name}
760
- avatar=${currentMsg.user?.avatar}
761
- ?system=${!email && !name}
910
+ avatar=${currentMsg._user?.avatar}
911
+ ?system=${isSystem}
762
912
  >
763
913
  </temba-user>
764
914
  </div>`
@@ -767,50 +917,58 @@ export class Chat extends RapidElement {
767
917
  `;
768
918
  }
769
919
 
770
- private renderMessage(event: ChatEvent, name = null): TemplateResult {
771
- if (
772
- event.type === MessageType.Error ||
773
- event.type === MessageType.Collapse ||
774
- event.type === MessageType.Inline
775
- ) {
776
- 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>`;
777
923
  }
778
924
 
779
- 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
+
780
934
  return html`
781
- <div class="bubble-wrap ${message.sendError ? 'error' : ''}">
782
- ${
783
- message.popup
784
- ? html`<div class="popup">
785
- ${message.popup}
786
- <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}
787
940
  </div>`
788
- : null
789
- }
790
-
791
- ${
792
- message.text
793
- ? html`
794
- <div class="bubble">
795
- ${name ? html`<div class="name">${name}</div>` : null}
796
- <div class="message message-text">${message.text}</div>
797
- <!--div>${message.date.toLocaleDateString(
798
- undefined,
799
- VERBOSE_FORMAT
800
- )}</div-->
801
- </div>
802
- `
803
- : null
804
- }
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}
805
964
 
806
- <div class="attachments">
807
- ${(message.attachments || []).map(
808
- (attachment) =>
809
- html`<temba-thumbnail
810
- attachment="${attachment}"
811
- ></temba-thumbnail>`
812
- )}
813
- </div>
965
+ <div class="attachments">
966
+ ${(message.msg.attachments || []).map(
967
+ (attachment) =>
968
+ html`<temba-thumbnail
969
+ attachment="${attachment}"
970
+ ></temba-thumbnail>`
971
+ )}
814
972
  </div>
815
973
  </div>
816
974
  `;
@@ -839,9 +997,15 @@ export class Chat extends RapidElement {
839
997
  >
840
998
  <div class="scroll" @scroll=${this.handleScroll}>
841
999
  ${this.messageGroups
842
- ? this.messageGroups.map(
843
- (msgGroup, idx, groups) =>
844
- 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
+ )}`
845
1009
  )
846
1010
  : null}
847
1011
 
@@ -858,6 +1022,16 @@ export class Chat extends RapidElement {
858
1022
  </div>`
859
1023
  : null}
860
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}
861
1035
  <slot class="header" name="header"></slot>
862
1036
  <slot class="footer" name="footer"></slot>
863
1037
  </div>`;
@@ -62,6 +62,9 @@ export class TembaUser extends RapidElement {
62
62
  @property({ type: String })
63
63
  email: string;
64
64
 
65
+ @property({ type: String })
66
+ uuid: string;
67
+
65
68
  @property({ type: String })
66
69
  avatar: string;
67
70
 
@@ -87,8 +90,6 @@ export class TembaUser extends RapidElement {
87
90
  if (changed.has('avatar')) {
88
91
  if (this.avatar) {
89
92
  this.bgimage = `url('${this.avatar}') center / contain no-repeat`;
90
- } else if (!this.system) {
91
- this.bgimage = null;
92
93
  }
93
94
  }
94
95
  }
package/src/events.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Msg, ObjectReference, User } from './interfaces';
2
+ import { ContactEvent } from './display/Chat';
2
3
 
3
4
  export interface EventGroup {
4
5
  type: string;
@@ -6,13 +7,6 @@ export interface EventGroup {
6
7
  open: boolean;
7
8
  }
8
9
 
9
- export interface ContactEvent {
10
- uuid?: string;
11
- type: string;
12
- created_on: string;
13
- _user?: ObjectReference;
14
- }
15
-
16
10
  export interface ChannelEvent extends ContactEvent {
17
11
  channel_event_type: string;
18
12
  duration: number;
@@ -57,7 +51,16 @@ export interface ChatStartedEvent extends ContactEvent {
57
51
  export interface MsgEvent extends ContactEvent {
58
52
  msg: Msg;
59
53
  optin?: ObjectReference;
60
- _status?: { created_on: string; status: string; reason: string };
54
+ _status?: {
55
+ created_on: string;
56
+ status: 'wired' | 'sent' | 'delivered' | 'read' | 'errored' | 'failed';
57
+ reason: 'error_limit' | 'too_old' | 'channel_removed';
58
+ };
59
+ _deleted?: {
60
+ created_on: string;
61
+ by_contact: boolean;
62
+ user: { name: string; uuid: string };
63
+ };
61
64
  _logs_url?: string;
62
65
  }
63
66
 
@@ -497,6 +497,16 @@ export class CanvasNode extends RapidElement {
497
497
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
498
498
  ): void {
499
499
  super.updated(changes);
500
+
501
+ if (!!changes.get('ui') && changes.has('ui')) {
502
+ // run revalidation every 50ms until 350ms to catch animation updates
503
+ for (let delay = 25; delay <= 350; delay += 25) {
504
+ setTimeout(() => {
505
+ this.plumber.revalidate([this.node.uuid]);
506
+ }, delay);
507
+ }
508
+ }
509
+
500
510
  if (changes.has('node')) {
501
511
  // Only proceed if plumber is available (for tests that don't set it up)
502
512
  if (this.plumber) {