@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.
- package/CHANGELOG.md +31 -1
- package/demo/components/flow/example.html +1 -0
- package/demo/components/webchat/example.html +1 -1
- package/demo/static/css/tailwind.css +30019 -0
- 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 +555 -476
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +248 -95
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +4 -4
- package/out-tsc/src/display/FloatingTab.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 +132 -58
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +183 -58
- 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/layout/FloatingWindow.js +1 -2
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/list/ContentMenu.js +1 -0
- package/out-tsc/src/list/ContentMenu.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +3 -2
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +184 -205
- 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 +34 -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/utils.js +3 -3
- package/out-tsc/src/utils.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/ActionHelper.js +6 -5
- package/out-tsc/test/ActionHelper.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-contact-chat.test.js +1 -1
- package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
- package/out-tsc/test/temba-floating-window.test.js +0 -2
- package/out-tsc/test/temba-floating-window.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 +195 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-utils-uuid.test.js +45 -1
- package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +2 -2
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
- package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- 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/screenshots/truth/floating-tab/default.png +0 -0
- package/screenshots/truth/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/hover.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +331 -135
- package/src/display/FloatingTab.ts +4 -4
- package/src/display/TembaUser.ts +3 -2
- package/src/events.ts +12 -12
- package/src/flow/CanvasNode.ts +140 -57
- package/src/flow/Editor.ts +240 -58
- package/src/flow/utils.ts +207 -1
- package/src/interfaces.ts +7 -0
- package/src/layout/FloatingWindow.ts +1 -3
- package/src/list/ContentMenu.ts +1 -0
- package/src/list/SortableList.ts +3 -2
- package/src/live/ContactChat.ts +195 -221
- 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 +43 -0
- package/src/store/Store.ts +5 -5
- package/src/utils.ts +3 -3
- package/src/webchat/WebChat.ts +24 -10
- package/test/ActionHelper.ts +13 -5
- package/test/actions/send_broadcast.test.ts +4 -2
- package/test/temba-contact-chat.test.ts +1 -1
- package/test/temba-floating-window.test.ts +0 -2
- package/test/temba-flow-collision.test.ts +833 -0
- package/test/temba-flow-editor-node.test.ts +224 -0
- package/test/temba-utils-uuid.test.ts +61 -1
- package/test/utils.test.ts +7 -2
- package/test-assets/contacts/history.json +22 -9
- package/web-test-runner.config.mjs +3 -3
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
|
-
|
|
18
|
-
|
|
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
|
|
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';
|
|
34
76
|
}
|
|
35
77
|
|
|
36
|
-
export interface
|
|
37
|
-
|
|
38
|
-
|
|
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-
|
|
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
|
-
.
|
|
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:
|
|
451
|
-
--thumb-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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:
|
|
704
|
+
private addMessage(msg: ContactEvent): boolean {
|
|
583
705
|
const isNew = !this.messageExists(msg);
|
|
584
|
-
this.msgMap.set(msg.
|
|
706
|
+
this.msgMap.set(msg.uuid, msg);
|
|
585
707
|
return isNew;
|
|
586
708
|
}
|
|
587
709
|
|
|
588
|
-
public messageExists(msg:
|
|
589
|
-
return this.msgMap.has(msg.
|
|
710
|
+
public messageExists(msg: ContactEvent): boolean {
|
|
711
|
+
return this.msgMap.has(msg.uuid);
|
|
590
712
|
}
|
|
591
713
|
|
|
592
|
-
private isSameGroup(msg1:
|
|
714
|
+
private isSameGroup(msg1: ContactEvent, msg2: ContactEvent): boolean {
|
|
593
715
|
if (msg1 && msg2) {
|
|
594
|
-
|
|
716
|
+
const sameGroup =
|
|
595
717
|
msg1.type === msg2.type &&
|
|
596
|
-
msg1.
|
|
597
|
-
Math.abs(msg1.
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
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 =
|
|
665
|
-
this.hideBottomScroll =
|
|
797
|
+
this.hideTopScroll = absScrollTop >= scrollableHeight - 1;
|
|
798
|
+
this.hideBottomScroll = absScrollTop <= 1;
|
|
666
799
|
|
|
667
|
-
|
|
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 =
|
|
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:
|
|
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(
|
|
703
|
-
|
|
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.
|
|
708
|
-
prevMsg.
|
|
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.
|
|
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.
|
|
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 !== '
|
|
722
|
-
: currentMsg.type === '
|
|
866
|
+
? currentMsg.type !== 'msg_received'
|
|
867
|
+
: currentMsg.type === 'msg_received';
|
|
723
868
|
|
|
724
|
-
const name = currentMsg.
|
|
725
|
-
const email = currentMsg.user?.email;
|
|
869
|
+
const name = currentMsg._user?.name;
|
|
726
870
|
|
|
727
871
|
const showAvatar =
|
|
728
|
-
((currentMsg.type === '
|
|
729
|
-
currentMsg.type === '
|
|
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
|
-
${
|
|
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
|
-
${
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
908
|
+
uuid=${currentMsg._user?.uuid}
|
|
751
909
|
name=${name}
|
|
752
|
-
avatar=${currentMsg.
|
|
753
|
-
?system=${
|
|
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:
|
|
764
|
-
if (
|
|
765
|
-
|
|
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
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
? html`<div
|
|
778
|
-
${
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
?
|
|
830
|
-
|
|
831
|
-
|
|
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>`;
|