@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.
- package/CHANGELOG.md +17 -0
- package/demo/components/webchat/example.html +1 -1
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +2 -11
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +307 -259
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +223 -90
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/TembaUser.js +3 -3
- package/out-tsc/src/display/TembaUser.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +8 -0
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +117 -28
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/utils.js +141 -0
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +122 -170
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +2 -11
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/store/AppState.js +3 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/store/Store.js +5 -5
- package/out-tsc/src/store/Store.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +22 -9
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/test/actions/send_broadcast.test.js +9 -4
- package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +673 -0
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor-node.test.js +128 -42
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/src/display/Chat.ts +303 -129
- package/src/display/TembaUser.ts +3 -2
- package/src/events.ts +11 -8
- package/src/flow/CanvasNode.ts +10 -0
- package/src/flow/Editor.ts +156 -28
- package/src/flow/utils.ts +207 -1
- package/src/interfaces.ts +7 -0
- package/src/live/ContactChat.ts +129 -180
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/store/AppState.ts +2 -0
- package/src/store/Store.ts +5 -5
- package/src/webchat/WebChat.ts +24 -10
- package/test/actions/send_broadcast.test.ts +2 -1
- package/test/temba-flow-collision.test.ts +833 -0
- package/test/temba-flow-editor-node.test.ts +142 -47
package/src/display/Chat.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
.
|
|
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:
|
|
452
|
-
--thumb-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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:
|
|
704
|
+
private addMessage(msg: ContactEvent): boolean {
|
|
590
705
|
const isNew = !this.messageExists(msg);
|
|
591
|
-
this.msgMap.set(msg.
|
|
706
|
+
this.msgMap.set(msg.uuid, msg);
|
|
592
707
|
return isNew;
|
|
593
708
|
}
|
|
594
709
|
|
|
595
|
-
public messageExists(msg:
|
|
596
|
-
return this.msgMap.has(msg.
|
|
710
|
+
public messageExists(msg: ContactEvent): boolean {
|
|
711
|
+
return this.msgMap.has(msg.uuid);
|
|
597
712
|
}
|
|
598
713
|
|
|
599
|
-
private isSameGroup(msg1:
|
|
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.
|
|
604
|
-
Math.abs(msg1.
|
|
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
|
|
668
|
-
const scroll = Math.round(top + ele.scrollTop);
|
|
669
|
-
const scrollPct = scroll / top;
|
|
783
|
+
const scrollableHeight = ele.scrollHeight - ele.clientHeight;
|
|
670
784
|
|
|
671
|
-
|
|
672
|
-
|
|
785
|
+
if (scrollableHeight <= 0) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
673
788
|
|
|
674
|
-
|
|
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 =
|
|
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:
|
|
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(
|
|
711
|
-
|
|
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.
|
|
716
|
-
prevMsg.
|
|
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.
|
|
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.
|
|
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 !== '
|
|
730
|
-
: currentMsg.type === '
|
|
866
|
+
? currentMsg.type !== 'msg_received'
|
|
867
|
+
: currentMsg.type === 'msg_received';
|
|
731
868
|
|
|
732
|
-
const name = currentMsg.
|
|
733
|
-
const email = currentMsg.user?.email;
|
|
869
|
+
const name = currentMsg._user?.name;
|
|
734
870
|
|
|
735
871
|
const showAvatar =
|
|
736
|
-
((currentMsg.type === '
|
|
737
|
-
currentMsg.type === '
|
|
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
|
-
${
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
908
|
+
uuid=${currentMsg._user?.uuid}
|
|
759
909
|
name=${name}
|
|
760
|
-
avatar=${currentMsg.
|
|
761
|
-
?system=${
|
|
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:
|
|
771
|
-
if (
|
|
772
|
-
|
|
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
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
? html`<div
|
|
785
|
-
${
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
?
|
|
843
|
-
|
|
844
|
-
|
|
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>`;
|
package/src/display/TembaUser.ts
CHANGED
|
@@ -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?: {
|
|
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
|
|
package/src/flow/CanvasNode.ts
CHANGED
|
@@ -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) {
|