@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,21 +1,49 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { html, css } from 'lit';
3
3
  import { property } from 'lit/decorators.js';
4
+ import { repeat } from 'lit/directives/repeat.js';
4
5
  import { RapidElement } from '../RapidElement';
5
6
  import { CustomEventType } from '../interfaces';
6
7
  import { DEFAULT_AVATAR } from '../webchat/assets';
7
- import { hashCode } from '../utils';
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
+ const getUnsendableReasonMessage = (reason) => {
12
+ switch (reason) {
13
+ case 'no_route':
14
+ return 'No channel available to send message';
15
+ case 'contact_blocked':
16
+ return 'Contact has been blocked';
17
+ case 'contact_stopped':
18
+ return 'Contact has been stopped';
19
+ case 'contact_archived':
20
+ return 'Contact is archived';
21
+ case 'org_suspended':
22
+ return 'Workspace is suspended';
23
+ case 'looping':
24
+ return 'Message loop detected';
25
+ default:
26
+ return 'Unable to send message';
27
+ }
28
+ };
29
+ const getStatusReasonMessage = (reason) => {
30
+ switch (reason) {
31
+ case 'error_limit':
32
+ return 'Error limit reached';
33
+ case 'too_old':
34
+ return 'Message is too old to send';
35
+ case 'channel_removed':
36
+ return 'Channel was removed';
37
+ default:
38
+ return 'Message failed to send';
39
+ }
40
+ };
11
41
  export var MessageType;
12
42
  (function (MessageType) {
13
43
  MessageType["Inline"] = "inline";
14
44
  MessageType["Error"] = "error";
15
45
  MessageType["Collapse"] = "collapse";
16
46
  MessageType["Note"] = "note";
17
- MessageType["MsgIn"] = "msg_in";
18
- MessageType["MsgOut"] = "msg_out";
19
47
  })(MessageType || (MessageType = {}));
20
48
  const TIME_FORMAT = { hour: 'numeric', minute: '2-digit' };
21
49
  const VERBOSE_FORMAT = {
@@ -37,7 +65,10 @@ export class Chat extends RapidElement {
37
65
  this.agent = false;
38
66
  this.endOfHistory = false;
39
67
  this.oldestEventDate = null;
68
+ this.showNewMessageNotification = false;
69
+ this.hasFooter = false;
40
70
  this.msgMap = new Map();
71
+ this.metadataCache = new Map();
41
72
  }
42
73
  static get styles() {
43
74
  return css `
@@ -207,16 +238,24 @@ export class Chat extends RapidElement {
207
238
  color: rgba(0, 0, 0, 0.5);
208
239
  }
209
240
 
241
+ .failed .bubble,
242
+ .error .bubble {
243
+ border: 1px solid var(--color-error);
244
+ background: #ffe6e6;
245
+ color: #ad4747ff;
246
+ }
247
+
248
+ .error .bubble .name,
249
+ .failed .bubble .name {
250
+ color: #ad47479a;
251
+ }
252
+
210
253
  .message {
211
254
  margin-bottom: 0.5em;
212
255
  line-height: 1.2em;
213
256
  word-break: break-word;
214
257
  }
215
258
 
216
- .message-text {
217
- white-space: pre-line;
218
- }
219
-
220
259
  .chat {
221
260
  width: 28rem;
222
261
  border-radius: var(--curvature);
@@ -427,19 +466,12 @@ export class Chat extends RapidElement {
427
466
  border-radius: var(--curvature);
428
467
  }
429
468
 
430
- .error .bubble {
431
- border: 1px solid var(--color-error);
432
- background: white;
433
- color: #333;
434
- }
435
-
436
- .error .bubble .name {
437
- color: #999;
438
- }
439
-
469
+ .failed temba-thumbnail,
440
470
  .error temba-thumbnail {
441
- --thumb-background: var(--color-error);
442
- --thumb-icon: white;
471
+ --thumb-background: #ffe6e6;
472
+ --thumb-border: var(--color-error);
473
+ border: 1px solid var(--color-error);
474
+ color: #ad4747a8;
443
475
  }
444
476
 
445
477
  .outgoing .popup {
@@ -483,6 +515,37 @@ export class Chat extends RapidElement {
483
515
  opacity: 1;
484
516
  transition-delay: 1s;
485
517
  }
518
+
519
+ .new-message-notification {
520
+ position: absolute;
521
+ bottom: 1em;
522
+ left: 50%;
523
+ transform: translateX(-50%) translateY(100px);
524
+ background: var(--color-primary-dark, #3c92dd);
525
+ color: white;
526
+ padding: 0.75em 1.5em;
527
+ border-radius: var(--curvature);
528
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 7px 0px,
529
+ rgba(0, 0, 0, 0.3) 0px 1px 2px 0px;
530
+ cursor: pointer;
531
+ opacity: 0;
532
+ transition: all 0.3s ease-out;
533
+ z-index: 100;
534
+ font-weight: 500;
535
+ pointer-events: none;
536
+ }
537
+
538
+ .new-message-notification.visible {
539
+ transform: translateX(-50%) translateY(0);
540
+ opacity: 1;
541
+ pointer-events: auto;
542
+ }
543
+
544
+ .new-message-notification:hover {
545
+ background: var(--color-primary-darker, #2b7ac4);
546
+ box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 10px 0px,
547
+ rgba(0, 0, 0, 0.4) 0px 2px 4px 0px;
548
+ }
486
549
  `;
487
550
  }
488
551
  firstUpdated(changed) {
@@ -493,15 +556,6 @@ export class Chat extends RapidElement {
493
556
  this.hideTopScroll = !hasScroll;
494
557
  }
495
558
  addMessages(messages, startTime = null, append = false) {
496
- // make sure our messages have ids
497
- messages.forEach((m) => {
498
- if (!m.id) {
499
- m.id =
500
- hashCode((m.text.strings || []).join('')) +
501
- '_' +
502
- m.date.toISOString();
503
- }
504
- });
505
559
  if (!startTime) {
506
560
  startTime = new Date();
507
561
  }
@@ -511,8 +565,16 @@ export class Chat extends RapidElement {
511
565
  // first add messages to the map
512
566
  const newMessages = [];
513
567
  for (const m of messages) {
568
+ // filter out metadata events - they aren't rendered but cached for later reference
569
+ if (m.type === 'msg_deleted' || m.type === 'msg_status_changed') {
570
+ const msgUuid = m.msg_uuid;
571
+ if (msgUuid) {
572
+ this.metadataCache.set(msgUuid, m);
573
+ }
574
+ continue;
575
+ }
514
576
  if (this.addMessage(m)) {
515
- newMessages.push(m.id);
577
+ newMessages.push(m.uuid);
516
578
  }
517
579
  }
518
580
  if (newMessages.length === 0) {
@@ -520,10 +582,25 @@ export class Chat extends RapidElement {
520
582
  }
521
583
  const ele = this.shadowRoot.querySelector('.scroll');
522
584
  const prevTop = ele.scrollTop;
585
+ const prevScrollHeight = ele.scrollHeight;
586
+ const scrollableHeight = ele.scrollHeight - ele.clientHeight;
587
+ const isScrolledAway = scrollableHeight > 0 && Math.abs(ele.scrollTop) > 50;
523
588
  const grouped = this.groupMessages(newMessages);
524
589
  this.insertGroups(grouped, append);
590
+ // show notification if new messages are appended and user is scrolled away from bottom
591
+ if (append && isScrolledAway && newMessages.length > 0) {
592
+ this.showNewMessageNotification = true;
593
+ }
525
594
  window.setTimeout(() => {
526
- ele.scrollTop = prevTop;
595
+ // when appending (new messages at bottom), adjust scroll to maintain visible content
596
+ // with column-reverse, new content at bottom increases scrollHeight
597
+ if (append && isScrolledAway) {
598
+ const heightDiff = ele.scrollHeight - prevScrollHeight;
599
+ ele.scrollTop = prevTop - heightDiff;
600
+ }
601
+ else {
602
+ ele.scrollTop = prevTop;
603
+ }
527
604
  this.fireCustomEvent(CustomEventType.FetchComplete);
528
605
  }, 100);
529
606
  },
@@ -534,18 +611,19 @@ export class Chat extends RapidElement {
534
611
  }
535
612
  addMessage(msg) {
536
613
  const isNew = !this.messageExists(msg);
537
- this.msgMap.set(msg.id, msg);
614
+ this.msgMap.set(msg.uuid, msg);
538
615
  return isNew;
539
616
  }
540
617
  messageExists(msg) {
541
- return this.msgMap.has(msg.id);
618
+ return this.msgMap.has(msg.uuid);
542
619
  }
543
620
  isSameGroup(msg1, msg2) {
544
621
  var _a, _b;
545
622
  if (msg1 && msg2) {
546
623
  const sameGroup = msg1.type === msg2.type &&
547
- ((_a = msg1.user) === null || _a === void 0 ? void 0 : _a.name) === ((_b = msg2.user) === null || _b === void 0 ? void 0 : _b.name) &&
548
- Math.abs(msg1.date.getTime() - msg2.date.getTime()) < BATCH_TIME_WINDOW;
624
+ ((_a = msg1._user) === null || _a === void 0 ? void 0 : _a.name) === ((_b = msg2._user) === null || _b === void 0 ? void 0 : _b.name) &&
625
+ Math.abs(msg1.created_on.getTime() - msg2.created_on.getTime()) <
626
+ BATCH_TIME_WINDOW;
549
627
  return sameGroup;
550
628
  }
551
629
  return false;
@@ -604,24 +682,39 @@ export class Chat extends RapidElement {
604
682
  }
605
683
  handleScroll(event) {
606
684
  const ele = event.target;
607
- const top = ele.scrollHeight - ele.clientHeight;
608
- const scroll = Math.round(top + ele.scrollTop);
609
- const scrollPct = scroll / top;
610
- this.hideTopScroll = scrollPct <= 0.01;
611
- this.hideBottomScroll = scrollPct >= 0.99;
612
- if (scrollPct < SCROLL_FETCH_BUFFER) {
685
+ const scrollableHeight = ele.scrollHeight - ele.clientHeight;
686
+ if (scrollableHeight <= 0) {
687
+ return;
688
+ }
689
+ // with column-reverse, scrollTop behavior depends on the browser
690
+ // check if scrollTop is negative (some browsers) or positive (others)
691
+ const absScrollTop = Math.abs(ele.scrollTop);
692
+ // when scrolling up to older messages, absScrollTop increases
693
+ // trigger when we're close to the maximum scroll (oldest messages)
694
+ const shouldFetch = absScrollTop >= scrollableHeight - SCROLL_FETCH_BUFFER;
695
+ this.hideTopScroll = absScrollTop >= scrollableHeight - 1;
696
+ this.hideBottomScroll = absScrollTop <= 1;
697
+ // hide notification when scrolled to bottom
698
+ if (absScrollTop <= 10) {
699
+ this.showNewMessageNotification = false;
700
+ }
701
+ if (shouldFetch) {
613
702
  this.fireCustomEvent(CustomEventType.ScrollThreshold);
614
703
  }
615
704
  }
616
705
  scrollToBottom() {
617
706
  const scroll = this.shadowRoot.querySelector('.scroll');
618
707
  if (scroll) {
619
- scroll.scrollTop = scroll.scrollHeight;
708
+ scroll.scrollTop = 0;
620
709
  this.hideBottomScroll = true;
710
+ this.showNewMessageNotification = false;
621
711
  }
622
712
  }
713
+ handleNewMessageClick() {
714
+ this.scrollToBottom();
715
+ }
623
716
  renderMessageGroup(msgIds, idx, groups) {
624
- var _a, _b, _c;
717
+ var _a, _b, _c, _d;
625
718
  const today = new Date();
626
719
  const firstGroup = idx === groups.length - 1;
627
720
  let prevMsg;
@@ -636,51 +729,58 @@ export class Chat extends RapidElement {
636
729
  let timeDisplay = null;
637
730
  if (prevMsg &&
638
731
  !this.isSameGroup(prevMsg, currentMsg) &&
639
- (Math.abs(currentMsg.date.getTime() - prevMsg.date.getTime()) >
640
- BATCH_TIME_WINDOW ||
732
+ (Math.abs(currentMsg.created_on.getTime() - prevMsg.created_on.getTime()) > BATCH_TIME_WINDOW ||
641
733
  idx === groups.length - 1)) {
642
- if (today.getDate() !== prevMsg.date.getDate() ||
643
- prevMsg.date.getDate() !== currentMsg.date.getDate()) {
734
+ if (today.getDate() !== prevMsg.created_on.getDate() ||
735
+ prevMsg.created_on.getDate() !== currentMsg.created_on.getDate()) {
644
736
  timeDisplay = html `<div class="time ${firstGroup ? 'first' : ''}">
645
- ${prevMsg.date.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
737
+ ${prevMsg.created_on.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
646
738
  </div>`;
647
739
  }
648
740
  else {
649
741
  timeDisplay = html `<div class="time ${firstGroup ? 'first' : ''}">
650
- ${prevMsg.date.toLocaleTimeString(undefined, TIME_FORMAT)}
742
+ ${prevMsg.created_on.toLocaleTimeString(undefined, TIME_FORMAT)}
651
743
  </div>`;
652
744
  }
653
745
  }
654
746
  const incoming = this.agent
655
- ? currentMsg.type !== 'msg_in'
656
- : currentMsg.type === 'msg_in';
657
- const name = (_a = currentMsg.user) === null || _a === void 0 ? void 0 : _a.name;
658
- const email = (_b = currentMsg.user) === null || _b === void 0 ? void 0 : _b.email;
659
- const showAvatar = ((currentMsg.type === 'note' ||
660
- currentMsg.type === 'msg_in' ||
661
- currentMsg.type === 'msg_out') &&
747
+ ? currentMsg.type !== 'msg_received'
748
+ : currentMsg.type === 'msg_received';
749
+ const name = (_a = currentMsg._user) === null || _a === void 0 ? void 0 : _a.name;
750
+ const showAvatar = ((currentMsg.type === 'msg_received' ||
751
+ currentMsg.type === 'msg_created') &&
662
752
  this.agent) ||
663
753
  !incoming;
754
+ const isSystem = !((_b = currentMsg._user) === null || _b === void 0 ? void 0 : _b.uuid);
664
755
  return html `
665
756
  ${timeDisplay}
666
- <div
667
- class="block ${incoming ? 'incoming' : 'outgoing'} ${currentMsg.type}"
668
- >
757
+ <div class="block ${incoming ? 'incoming' : 'outgoing'}">
669
758
  <div class="group-messages" style="flex-grow:1">
670
- ${msgIds.map((msgId, index) => {
759
+ ${repeat(msgIds, (msgId) => msgId, (msgId, index) => {
760
+ var _a, _b;
671
761
  const msg = this.msgMap.get(msgId);
672
- return html `<div class="row message">
673
- ${this.renderMessage(msg, index == 0 ? name : null)}
674
- </div>`;
762
+ const msgEvent = msg;
763
+ const statusClass = msg._status
764
+ ? msg._status.status
765
+ : '';
766
+ const hasError = ((_a = msgEvent.msg) === null || _a === void 0 ? void 0 : _a.unsendable_reason) ||
767
+ (((_b = msgEvent._status) === null || _b === void 0 ? void 0 : _b.reason) &&
768
+ (statusClass === 'failed' || statusClass === 'errored'));
769
+ const unsendableClass = hasError ? 'error' : '';
770
+ return html `<div
771
+ class="row message ${statusClass} ${unsendableClass}"
772
+ >
773
+ ${this.renderMessage(msg, index == 0 ? name : null)}
774
+ </div>`;
675
775
  })}
676
776
  </div>
677
777
  ${showAvatar
678
778
  ? html `<div class="avatar" style="align-self:flex-end">
679
779
  <temba-user
680
- email=${email}
780
+ uuid=${(_c = currentMsg._user) === null || _c === void 0 ? void 0 : _c.uuid}
681
781
  name=${name}
682
- avatar=${(_c = currentMsg.user) === null || _c === void 0 ? void 0 : _c.avatar}
683
- ?system=${!email && !name}
782
+ avatar=${(_d = currentMsg._user) === null || _d === void 0 ? void 0 : _d.avatar}
783
+ ?system=${isSystem}
684
784
  >
685
785
  </temba-user>
686
786
  </div>`
@@ -689,36 +789,53 @@ export class Chat extends RapidElement {
689
789
  `;
690
790
  }
691
791
  renderMessage(event, name = null) {
692
- if (event.type === MessageType.Error ||
693
- event.type === MessageType.Collapse ||
694
- event.type === MessageType.Inline) {
695
- return html `<div class="event">${event.text}</div>`;
792
+ var _a, _b;
793
+ if (event._rendered) {
794
+ return html `<div class="event">${event._rendered.html}</div>`;
696
795
  }
697
796
  const message = event;
797
+ const unsendableReason = (_a = message.msg) === null || _a === void 0 ? void 0 : _a.unsendable_reason;
798
+ const statusReason = (_b = message._status) === null || _b === void 0 ? void 0 : _b.reason;
799
+ const errorMessage = unsendableReason
800
+ ? getUnsendableReasonMessage(unsendableReason)
801
+ : statusReason
802
+ ? getStatusReasonMessage(statusReason)
803
+ : null;
698
804
  return html `
699
- <div class="bubble-wrap ${message.sendError ? 'error' : ''}">
700
- ${message.popup
701
- ? html `<div class="popup">
702
- ${message.popup}
703
- <div class="arrow">▼</div>
805
+ <div class="bubble-wrap">
806
+ <div class="popup" style="white-space: nowrap;">
807
+ ${errorMessage
808
+ ? html `<div style="color: var(--color-error); margin-right: 1em;">
809
+ ${errorMessage}
704
810
  </div>`
705
811
  : null}
706
-
707
- ${message.text
708
- ? html `
709
- <div class="bubble">
710
- ${name ? html `<div class="name">${name}</div>` : null}
711
- <div class="message message-text">${message.text}</div>
712
- <!--div>${message.date.toLocaleDateString(undefined, VERBOSE_FORMAT)}</div-->
713
- </div>
714
- `
812
+ <temba-date
813
+ value="${message.created_on.toISOString()}"
814
+ display="relative"
815
+ ></temba-date>
816
+ ${message._logs_url
817
+ ? html `<a
818
+ style="margin-left: 1em; color: var(--color-primary-dark);"
819
+ href="${message._logs_url}"
820
+ target="_blank"
821
+ rel="noopener noreferrer"
822
+ ><temba-icon name="log"></temba-icon
823
+ ></a>`
824
+ : null}
825
+
826
+ <div class="arrow">▼</div>
827
+ </div>
828
+ ${message.msg.text
829
+ ? html `<div class="bubble">
830
+ ${name ? html `<div class="name">${name}</div>` : null}
831
+ <div class="message message-text">${message.msg.text}</div>
832
+ </div>`
715
833
  : null}
716
834
 
717
- <div class="attachments">
718
- ${(message.attachments || []).map((attachment) => html `<temba-thumbnail
719
- attachment="${attachment}"
720
- ></temba-thumbnail>`)}
721
- </div>
835
+ <div class="attachments">
836
+ ${(message.msg.attachments || []).map((attachment) => html `<temba-thumbnail
837
+ attachment="${attachment}"
838
+ ></temba-thumbnail>`)}
722
839
  </div>
723
840
  </div>
724
841
  `;
@@ -744,7 +861,7 @@ export class Chat extends RapidElement {
744
861
  >
745
862
  <div class="scroll" @scroll=${this.handleScroll}>
746
863
  ${this.messageGroups
747
- ? this.messageGroups.map((msgGroup, idx, groups) => html `${this.renderMessageGroup(msgGroup, idx, groups)}`)
864
+ ? repeat(this.messageGroups, (msgGroup) => msgGroup.join(','), (msgGroup, idx) => html `${this.renderMessageGroup(msgGroup, idx, this.messageGroups)}`)
748
865
  : null}
749
866
 
750
867
  <temba-loading
@@ -757,6 +874,16 @@ export class Chat extends RapidElement {
757
874
  </div>`
758
875
  : null}
759
876
  </div>
877
+ ${!this.hasFooter
878
+ ? html `<div
879
+ class="new-message-notification ${this.showNewMessageNotification
880
+ ? 'visible'
881
+ : ''}"
882
+ @click=${this.handleNewMessageClick}
883
+ >
884
+ New Messages
885
+ </div>`
886
+ : null}
760
887
  <slot class="header" name="header"></slot>
761
888
  <slot class="footer" name="footer"></slot>
762
889
  </div>`;
@@ -786,4 +913,10 @@ __decorate([
786
913
  __decorate([
787
914
  property({ type: Object, attribute: false })
788
915
  ], Chat.prototype, "oldestEventDate", void 0);
916
+ __decorate([
917
+ property({ type: Boolean, attribute: false })
918
+ ], Chat.prototype, "showNewMessageNotification", void 0);
919
+ __decorate([
920
+ property({ type: Boolean })
921
+ ], Chat.prototype, "hasFooter", void 0);
789
922
  //# sourceMappingURL=Chat.js.map