@nyaruka/temba-components 0.91.6 → 0.92.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 +9 -0
- package/demo/index.html +1 -1
- package/dist/temba-components.js +760 -1189
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chat/Chat.js +714 -0
- package/out-tsc/src/chat/Chat.js.map +1 -0
- package/out-tsc/src/completion/helpers.js +1 -29
- package/out-tsc/src/completion/helpers.js.map +1 -1
- package/out-tsc/src/compose/Compose.js +6 -2
- package/out-tsc/src/compose/Compose.js.map +1 -1
- package/out-tsc/src/contacts/ContactChat.js +518 -54
- package/out-tsc/src/contacts/ContactChat.js.map +1 -1
- package/out-tsc/src/contacts/events.js +1 -998
- package/out-tsc/src/contacts/events.js.map +1 -1
- package/out-tsc/src/lightbox/Lightbox.js +4 -0
- package/out-tsc/src/lightbox/Lightbox.js.map +1 -1
- package/out-tsc/src/list/TembaMenu.js +0 -1
- package/out-tsc/src/list/TembaMenu.js.map +1 -1
- package/out-tsc/src/markdown.js +33 -0
- package/out-tsc/src/markdown.js.map +1 -0
- package/out-tsc/src/select/Select.js +6 -1
- package/out-tsc/src/select/Select.js.map +1 -1
- package/out-tsc/src/textinput/TextInput.js +1 -1
- package/out-tsc/src/textinput/TextInput.js.map +1 -1
- package/out-tsc/src/thumbnail/Thumbnail.js +128 -81
- package/out-tsc/src/thumbnail/Thumbnail.js.map +1 -1
- package/out-tsc/src/utils/index.js +9 -11
- package/out-tsc/src/utils/index.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +109 -358
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/src/webchat/index.js +17 -0
- package/out-tsc/src/webchat/index.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -2
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/temba-webchat.js +2 -0
- package/out-tsc/temba-webchat.js.map +1 -1
- package/out-tsc/test/temba-contact-chat.test.js +1 -0
- package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
- package/out-tsc/test/temba-lightbox.test.js +4 -4
- package/out-tsc/test/temba-lightbox.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
- package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
- package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
- package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
- package/screenshots/truth/contacts/contact-active-default.png +0 -0
- package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
- package/screenshots/truth/contacts/contact-archived-hide-chatbox.png +0 -0
- package/screenshots/truth/contacts/contact-blocked-hide-chatbox.png +0 -0
- package/screenshots/truth/contacts/contact-stopped-hide-chatbox.png +0 -0
- package/screenshots/truth/lightbox/img-zoomed.png +0 -0
- package/screenshots/truth/lightbox/img.png +0 -0
- package/src/chat/Chat.ts +791 -0
- package/src/completion/helpers.ts +2 -40
- package/src/compose/Compose.ts +6 -2
- package/src/contacts/ContactChat.ts +609 -59
- package/src/contacts/events.ts +1 -1068
- package/src/lightbox/Lightbox.ts +5 -0
- package/src/list/TembaMenu.ts +0 -1
- package/src/markdown.ts +41 -0
- package/src/select/Select.ts +5 -1
- package/src/textinput/TextInput.ts +1 -1
- package/src/thumbnail/Thumbnail.ts +130 -81
- package/src/utils/index.ts +12 -13
- package/src/webchat/WebChat.ts +196 -413
- package/src/webchat/index.ts +23 -1
- package/static/css/temba-components.css +2 -0
- package/temba-modules.ts +2 -2
- package/temba-webchat.ts +2 -0
- package/test/temba-contact-chat.test.ts +1 -0
- package/test/temba-lightbox.test.ts +4 -4
- package/test-assets/contacts/history.json +1 -56
- package/out-tsc/src/contacts/ContactHistory.js +0 -691
- package/out-tsc/src/contacts/ContactHistory.js.map +0 -1
- package/out-tsc/test/temba-contact-history.test.js +0 -69
- package/out-tsc/test/temba-contact-history.test.js.map +0 -1
- package/src/contacts/ContactHistory.ts +0 -875
- package/test/temba-contact-history.test.ts +0 -107
package/src/webchat/WebChat.ts
CHANGED
|
@@ -3,24 +3,78 @@ import { LitElement, TemplateResult, html, css, PropertyValueMap } from 'lit';
|
|
|
3
3
|
import { property } from 'lit/decorators.js';
|
|
4
4
|
import { getCookie, setCookie } from '../utils';
|
|
5
5
|
import { DEFAULT_AVATAR } from './assets';
|
|
6
|
+
import { Chat, ChatEvent, Message, MessageType } from '../chat/Chat';
|
|
6
7
|
|
|
7
8
|
interface User {
|
|
8
9
|
avatar?: string;
|
|
9
10
|
email: string;
|
|
10
11
|
name: string;
|
|
12
|
+
id: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
interface
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
interface MsgIn {
|
|
16
|
+
id: string;
|
|
17
|
+
text: string;
|
|
18
|
+
time: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MsgOut extends MsgIn {
|
|
22
|
+
user: User;
|
|
23
|
+
origin: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SockMsg {
|
|
27
|
+
type:
|
|
28
|
+
| 'start_chat'
|
|
29
|
+
| 'get_history'
|
|
30
|
+
| 'send_msg'
|
|
31
|
+
| 'ack_chat'
|
|
32
|
+
|
|
33
|
+
// responses
|
|
34
|
+
| 'chat_started'
|
|
35
|
+
| 'chat_resumed'
|
|
36
|
+
| 'msg_in'
|
|
37
|
+
| 'msg_out'
|
|
38
|
+
| 'history'
|
|
39
|
+
|
|
40
|
+
// receiving
|
|
41
|
+
| 'chat_out';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface GetHistoryCmd extends SockMsg {
|
|
20
45
|
before?: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StartChatCmd extends SockMsg {
|
|
49
|
+
chat_id?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SendMsgCmd extends SockMsg {
|
|
53
|
+
text: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Ack extends SockMsg {
|
|
57
|
+
msg_id: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface MsgOutResponse extends SockMsg {
|
|
61
|
+
msg_out: MsgOut;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface MsgInResponse extends SockMsg {
|
|
65
|
+
msg_in: MsgIn;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface HistoryResponse extends SockMsg {
|
|
69
|
+
history: (MsgInResponse | MsgOutResponse)[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface StartChatResponse extends SockMsg {
|
|
73
|
+
chat_id: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ResumeChatResponse extends StartChatResponse {
|
|
77
|
+
email?: string;
|
|
24
78
|
}
|
|
25
79
|
|
|
26
80
|
enum ChatStatus {
|
|
@@ -29,26 +83,19 @@ enum ChatStatus {
|
|
|
29
83
|
CONNECTED = 'connected'
|
|
30
84
|
}
|
|
31
85
|
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
weekday: undefined,
|
|
46
|
-
year: undefined,
|
|
47
|
-
month: 'short',
|
|
48
|
-
day: 'numeric',
|
|
49
|
-
hour: 'numeric',
|
|
50
|
-
minute: '2-digit'
|
|
51
|
-
} as any;
|
|
86
|
+
const sockToChat = function (msg: any): ChatEvent | Message {
|
|
87
|
+
const type = msg.msg_in ? MessageType.MsgIn : MessageType.MsgOut;
|
|
88
|
+
const msgContent = msg.msg_in || msg.msg_out;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: msgContent.id,
|
|
92
|
+
type,
|
|
93
|
+
text: msgContent.text,
|
|
94
|
+
date: new Date(msgContent.time),
|
|
95
|
+
user: msgContent.user,
|
|
96
|
+
attachments: msgContent.attachments
|
|
97
|
+
};
|
|
98
|
+
};
|
|
52
99
|
|
|
53
100
|
export class WebChat extends LitElement {
|
|
54
101
|
static get styles() {
|
|
@@ -129,6 +176,11 @@ export class WebChat extends LitElement {
|
|
|
129
176
|
background: #fff;
|
|
130
177
|
}
|
|
131
178
|
|
|
179
|
+
.border {
|
|
180
|
+
border-top: 1px solid #e9e9e9;
|
|
181
|
+
margin: 0 1em;
|
|
182
|
+
}
|
|
183
|
+
|
|
132
184
|
.avatar {
|
|
133
185
|
margin-top: 0.6em;
|
|
134
186
|
margin-right: 0.6em;
|
|
@@ -201,6 +253,7 @@ export class WebChat extends LitElement {
|
|
|
201
253
|
}
|
|
202
254
|
|
|
203
255
|
.chat {
|
|
256
|
+
height: 40rem;
|
|
204
257
|
width: 28rem;
|
|
205
258
|
border-radius: var(--curvature);
|
|
206
259
|
overflow: hidden;
|
|
@@ -213,6 +266,9 @@ export class WebChat extends LitElement {
|
|
|
213
266
|
transform: scale(0.9);
|
|
214
267
|
pointer-events: none;
|
|
215
268
|
opacity: 0;
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-direction: column;
|
|
271
|
+
background: #fff;
|
|
216
272
|
}
|
|
217
273
|
|
|
218
274
|
.chat.open {
|
|
@@ -222,61 +278,6 @@ export class WebChat extends LitElement {
|
|
|
222
278
|
pointer-events: initial;
|
|
223
279
|
}
|
|
224
280
|
|
|
225
|
-
.messages {
|
|
226
|
-
background: #fff;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.scroll {
|
|
230
|
-
height: 40rem;
|
|
231
|
-
max-height: 60vh;
|
|
232
|
-
overflow: auto;
|
|
233
|
-
-webkit-overflow-scrolling: touch;
|
|
234
|
-
overflow-scrolling: touch;
|
|
235
|
-
padding: 1em 1em 0 1em;
|
|
236
|
-
display: flex;
|
|
237
|
-
flex-direction: column-reverse;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.messages:before {
|
|
241
|
-
content: '';
|
|
242
|
-
background: /* Shadow TOP */ radial-gradient(
|
|
243
|
-
farthest-side at 50% 0,
|
|
244
|
-
rgba(0, 0, 0, 0.2),
|
|
245
|
-
rgba(0, 0, 0, 0)
|
|
246
|
-
)
|
|
247
|
-
center top;
|
|
248
|
-
height: 10px;
|
|
249
|
-
display: block;
|
|
250
|
-
position: absolute;
|
|
251
|
-
width: 28rem;
|
|
252
|
-
transition: opacity var(--toggle-speed) ease-out;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
.messages:after {
|
|
256
|
-
content: '';
|
|
257
|
-
background: /* Shadow BOTTOM */ radial-gradient(
|
|
258
|
-
farthest-side at 50% 100%,
|
|
259
|
-
rgba(0, 0, 0, 0.2),
|
|
260
|
-
rgba(0, 0, 0, 0)
|
|
261
|
-
)
|
|
262
|
-
center bottom;
|
|
263
|
-
height: 10px;
|
|
264
|
-
display: block;
|
|
265
|
-
position: absolute;
|
|
266
|
-
margin-top: -10px;
|
|
267
|
-
width: 28rem;
|
|
268
|
-
margin-right: 5em;
|
|
269
|
-
transition: opacity var(--toggle-speed) ease-out;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.scroll-at-top .messages:before {
|
|
273
|
-
opacity: 0;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.scroll-at-bottom .messages:after {
|
|
277
|
-
opacity: 0;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
281
|
.input {
|
|
281
282
|
border: none;
|
|
282
283
|
flex-grow: 1;
|
|
@@ -369,44 +370,47 @@ export class WebChat extends LitElement {
|
|
|
369
370
|
@property({ type: Boolean })
|
|
370
371
|
open = false;
|
|
371
372
|
|
|
372
|
-
@property({ type: Boolean })
|
|
373
|
-
fetching = false;
|
|
374
|
-
|
|
375
373
|
@property({ type: Boolean })
|
|
376
374
|
hasPendingText = false;
|
|
377
375
|
|
|
378
|
-
@property({ type: Boolean, attribute: false })
|
|
379
|
-
hideTopScroll = true;
|
|
380
|
-
|
|
381
|
-
@property({ type: Boolean, attribute: false })
|
|
382
|
-
hideBottomScroll = true;
|
|
383
|
-
|
|
384
|
-
@property({ type: Boolean, attribute: false })
|
|
385
|
-
blockHistoryFetching = false;
|
|
386
|
-
|
|
387
376
|
@property({ type: String })
|
|
388
377
|
host: string;
|
|
389
378
|
|
|
390
379
|
@property({ type: String })
|
|
391
380
|
activeUserAvatar: string;
|
|
392
381
|
|
|
393
|
-
|
|
382
|
+
@property({ type: Boolean, attribute: false })
|
|
383
|
+
blockHistoryFetching = false;
|
|
384
|
+
|
|
385
|
+
private chat: Chat;
|
|
394
386
|
private sock: WebSocket;
|
|
395
387
|
private newMessageCount = 0;
|
|
396
|
-
private oldestMessageDate: Date;
|
|
397
388
|
private fetchRequested: Date;
|
|
389
|
+
private beforeTime: string;
|
|
398
390
|
|
|
399
391
|
public constructor() {
|
|
400
392
|
super();
|
|
401
393
|
}
|
|
402
394
|
|
|
395
|
+
public firstUpdated(
|
|
396
|
+
changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
397
|
+
): void {
|
|
398
|
+
super.firstUpdated(changed);
|
|
399
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
400
|
+
|
|
401
|
+
const lightbox = document.createElement('temba-lightbox');
|
|
402
|
+
document.querySelector('body').appendChild(lightbox);
|
|
403
|
+
}
|
|
404
|
+
|
|
403
405
|
private handleReconnect() {
|
|
404
406
|
this.openSocket();
|
|
405
407
|
}
|
|
406
408
|
|
|
407
|
-
private sendSockMessage(
|
|
408
|
-
|
|
409
|
-
|
|
409
|
+
private sendSockMessage(
|
|
410
|
+
cmd: GetHistoryCmd | StartChatCmd | SendMsgCmd | Ack
|
|
411
|
+
) {
|
|
412
|
+
console.log('out', cmd);
|
|
413
|
+
this.sock.send(JSON.stringify(cmd));
|
|
410
414
|
}
|
|
411
415
|
|
|
412
416
|
private openSocket(): void {
|
|
@@ -416,172 +420,94 @@ export class WebChat extends LitElement {
|
|
|
416
420
|
|
|
417
421
|
this.status = ChatStatus.CONNECTING;
|
|
418
422
|
const webChat = this;
|
|
419
|
-
let url = `wss://localhost.textit.com/connect/${this.channel}/`;
|
|
423
|
+
let url = `wss://localhost.textit.com/wc/connect/${this.channel}/`;
|
|
420
424
|
if (this.urn) {
|
|
421
425
|
url = `${url}?chat_id=${this.urn}`;
|
|
422
426
|
}
|
|
423
427
|
const sock = new WebSocket(url);
|
|
424
428
|
this.sock = sock;
|
|
425
|
-
this.sock.onclose = function (
|
|
426
|
-
console.log('Socket closed', event);
|
|
429
|
+
this.sock.onclose = function () {
|
|
427
430
|
webChat.status = ChatStatus.DISCONNECTED;
|
|
428
431
|
};
|
|
429
432
|
|
|
430
|
-
this.sock.onopen = function (
|
|
431
|
-
|
|
433
|
+
this.sock.onopen = function () {
|
|
434
|
+
webChat.beforeTime = new Date().toISOString();
|
|
432
435
|
webChat.status = ChatStatus.CONNECTED;
|
|
433
436
|
webChat.urn = getCookie('temba-chat-urn');
|
|
434
|
-
const
|
|
437
|
+
const cmd: StartChatCmd = { type: 'start_chat' };
|
|
435
438
|
if (webChat.urn) {
|
|
436
|
-
|
|
439
|
+
cmd.chat_id = webChat.urn;
|
|
437
440
|
}
|
|
438
|
-
webChat.sendSockMessage(
|
|
441
|
+
webChat.sendSockMessage(cmd);
|
|
439
442
|
};
|
|
440
443
|
|
|
441
444
|
this.sock.onmessage = function (event: MessageEvent) {
|
|
442
445
|
webChat.status = ChatStatus.CONNECTED;
|
|
443
|
-
const msg = JSON.parse(event.data) as
|
|
444
|
-
console.log('
|
|
445
|
-
|
|
446
|
+
const msg = JSON.parse(event.data) as SockMsg;
|
|
447
|
+
console.log('in', msg);
|
|
446
448
|
if (msg.type === 'chat_started') {
|
|
447
|
-
|
|
449
|
+
const response = msg as StartChatResponse;
|
|
450
|
+
if (webChat.urn !== response.chat_id) {
|
|
448
451
|
webChat.messageGroups = [];
|
|
449
452
|
}
|
|
450
|
-
webChat.urn =
|
|
451
|
-
setCookie('temba-chat-urn',
|
|
453
|
+
webChat.urn = response.chat_id;
|
|
454
|
+
setCookie('temba-chat-urn', response.chat_id);
|
|
452
455
|
webChat.requestUpdate('messageGroups');
|
|
453
456
|
} else if (msg.type === 'chat_resumed') {
|
|
454
|
-
|
|
455
|
-
webChat.urn =
|
|
457
|
+
const response = msg as ResumeChatResponse;
|
|
458
|
+
webChat.urn = response.chat_id;
|
|
456
459
|
webChat.fetchPreviousMessages();
|
|
457
|
-
} else if (msg.type === '
|
|
458
|
-
|
|
459
|
-
webChat.
|
|
460
|
+
} else if (msg.type === 'chat_out') {
|
|
461
|
+
const response = msg as MsgOutResponse;
|
|
462
|
+
webChat.chat.addMessages([sockToChat(response)], null, true);
|
|
463
|
+
|
|
464
|
+
// ack receipt
|
|
465
|
+
const ack: Ack = { type: 'ack_chat', msg_id: response.msg_out.id };
|
|
466
|
+
webChat.sendSockMessage(ack);
|
|
460
467
|
} else if (msg.type === 'history') {
|
|
461
|
-
webChat.handleHistoryResponse(msg);
|
|
468
|
+
webChat.handleHistoryResponse(msg as HistoryResponse);
|
|
462
469
|
}
|
|
463
470
|
};
|
|
464
|
-
}
|
|
465
471
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
msg1.origin === msg2.origin &&
|
|
470
|
-
msg1.user?.name === msg2.user?.name &&
|
|
471
|
-
Math.abs(msg1.timeAsDate.getTime() - msg2.timeAsDate.getTime()) <
|
|
472
|
-
BATCH_TIME_WINDOW
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private insertGroups(newGroups: string[][], append = false) {
|
|
479
|
-
newGroups.reverse();
|
|
480
|
-
for (const newGroup of newGroups) {
|
|
481
|
-
// see if our new group belongs to the most recent group
|
|
482
|
-
const group =
|
|
483
|
-
this.messageGroups[append ? 0 : this.messageGroups.length - 1];
|
|
484
|
-
|
|
485
|
-
if (group) {
|
|
486
|
-
const lastMsgId = group[group.length - 1];
|
|
487
|
-
const lastMsg = this.msgMap.get(lastMsgId);
|
|
488
|
-
const newMsg = this.msgMap.get(newGroup[0]);
|
|
489
|
-
// if our message belongs to the previous group, in we go
|
|
490
|
-
if (this.isSameGroup(lastMsg, newMsg)) {
|
|
491
|
-
group.push(...newGroup);
|
|
492
|
-
} else {
|
|
493
|
-
// otherwise, just add our entire group as a new one
|
|
494
|
-
if (append) {
|
|
495
|
-
this.messageGroups.splice(0, 0, newGroup);
|
|
496
|
-
} else {
|
|
497
|
-
this.messageGroups.push(newGroup);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
if (append) {
|
|
502
|
-
this.messageGroups.splice(0, 0, newGroup);
|
|
503
|
-
} else {
|
|
504
|
-
this.messageGroups.push(newGroup);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
this.requestUpdate('messageGroups');
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
private groupMessages(msgIds: string[]): string[][] {
|
|
513
|
-
// group our messages by origin and user
|
|
514
|
-
const groups = [];
|
|
515
|
-
let lastGroup = [];
|
|
516
|
-
let lastMsg = null;
|
|
517
|
-
for (const msgId of msgIds) {
|
|
518
|
-
const msg = this.msgMap.get(msgId);
|
|
519
|
-
if (!this.isSameGroup(msg, lastMsg)) {
|
|
520
|
-
lastGroup = [];
|
|
521
|
-
groups.push(lastGroup);
|
|
522
|
-
}
|
|
523
|
-
lastGroup.push(msgId);
|
|
524
|
-
lastMsg = msg;
|
|
525
|
-
}
|
|
526
|
-
return groups;
|
|
472
|
+
this.sock.onerror = function () {
|
|
473
|
+
webChat.status = ChatStatus.DISCONNECTED;
|
|
474
|
+
};
|
|
527
475
|
}
|
|
528
476
|
|
|
529
|
-
|
|
477
|
+
public fetchPreviousMessages() {
|
|
530
478
|
if (!this.blockHistoryFetching) {
|
|
479
|
+
this.fetchRequested = new Date();
|
|
531
480
|
this.blockHistoryFetching = true;
|
|
532
|
-
this.fetching = true;
|
|
533
|
-
|
|
534
|
-
const getHistoryMsg = { type: 'get_history' };
|
|
535
|
-
if (this.oldestMessageDate) {
|
|
536
|
-
getHistoryMsg['before'] = this.oldestMessageDate.toISOString();
|
|
537
|
-
}
|
|
481
|
+
this.chat.fetching = true;
|
|
538
482
|
|
|
483
|
+
const cmd: GetHistoryCmd = {
|
|
484
|
+
type: 'get_history',
|
|
485
|
+
before: this.beforeTime
|
|
486
|
+
};
|
|
539
487
|
this.fetchRequested = new Date();
|
|
540
|
-
this.sendSockMessage(
|
|
488
|
+
this.sendSockMessage(cmd);
|
|
541
489
|
}
|
|
542
490
|
}
|
|
543
491
|
|
|
544
|
-
private handleHistoryResponse(
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
newMessages.push(m.msg_id);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (newMessages.length === 0) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
492
|
+
private handleHistoryResponse(response: HistoryResponse) {
|
|
493
|
+
const messages = response.history.reverse();
|
|
494
|
+
if (messages.length > 0) {
|
|
495
|
+
const oldestMessage = messages[0];
|
|
496
|
+
if (oldestMessage['msg_in']) {
|
|
497
|
+
const msgIn = (oldestMessage as MsgInResponse).msg_in;
|
|
498
|
+
this.beforeTime = msgIn.time;
|
|
499
|
+
} else if (oldestMessage['msg_out']) {
|
|
500
|
+
const msgOut = (oldestMessage as MsgOutResponse).msg_out;
|
|
501
|
+
this.beforeTime = msgOut.time;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
563
504
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const ele = this.shadowRoot.querySelector('.scroll');
|
|
567
|
-
const prevTop = ele.scrollTop;
|
|
568
|
-
|
|
569
|
-
window.setTimeout(() => {
|
|
570
|
-
ele.scrollTop = prevTop;
|
|
571
|
-
this.blockHistoryFetching = false;
|
|
572
|
-
}, 100);
|
|
573
|
-
},
|
|
574
|
-
// if it's the first load don't wait, otherwise wait a minimum amount of time
|
|
575
|
-
this.messageGroups.length === 0
|
|
576
|
-
? 0
|
|
577
|
-
: Math.max(0, MIN_FETCH_TIME - elapsed)
|
|
578
|
-
);
|
|
505
|
+
// convert messages to chat messages
|
|
506
|
+
this.chat.addMessages(messages.map(sockToChat), this.fetchRequested);
|
|
579
507
|
}
|
|
580
508
|
|
|
581
|
-
public
|
|
582
|
-
|
|
583
|
-
): void {
|
|
584
|
-
super.firstUpdated(changed);
|
|
509
|
+
public fetchComplete() {
|
|
510
|
+
this.blockHistoryFetching = false;
|
|
585
511
|
}
|
|
586
512
|
|
|
587
513
|
private focusInput() {
|
|
@@ -597,12 +523,6 @@ export class WebChat extends LitElement {
|
|
|
597
523
|
super.updated(changed);
|
|
598
524
|
|
|
599
525
|
if (this.open && changed.has('open') && changed.get('open') !== undefined) {
|
|
600
|
-
const scroll = this.shadowRoot.querySelector('.scroll');
|
|
601
|
-
const hasScroll = scroll.scrollHeight > scroll.clientHeight;
|
|
602
|
-
this.hideBottomScroll = true;
|
|
603
|
-
this.hideTopScroll = !hasScroll;
|
|
604
|
-
this.scrollToBottom();
|
|
605
|
-
|
|
606
526
|
if (this.status === ChatStatus.DISCONNECTED) {
|
|
607
527
|
this.openSocket();
|
|
608
528
|
}
|
|
@@ -615,27 +535,6 @@ export class WebChat extends LitElement {
|
|
|
615
535
|
}
|
|
616
536
|
}
|
|
617
537
|
|
|
618
|
-
private addMessage(msg: Message): boolean {
|
|
619
|
-
if (msg.time && !msg.timeAsDate) {
|
|
620
|
-
msg.timeAsDate = new Date(msg.time);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (
|
|
624
|
-
!this.oldestMessageDate ||
|
|
625
|
-
msg.timeAsDate.getTime() < this.oldestMessageDate.getTime()
|
|
626
|
-
) {
|
|
627
|
-
this.oldestMessageDate = msg.timeAsDate;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const isNew = !this.msgMap.has(msg.msg_id);
|
|
631
|
-
this.msgMap.set(msg.msg_id, msg);
|
|
632
|
-
|
|
633
|
-
if (msg.user?.avatar) {
|
|
634
|
-
this.activeUserAvatar = msg.user.avatar;
|
|
635
|
-
}
|
|
636
|
-
return isNew;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
538
|
public openChat(): void {
|
|
640
539
|
this.open = true;
|
|
641
540
|
}
|
|
@@ -654,125 +553,23 @@ export class WebChat extends LitElement {
|
|
|
654
553
|
const text = input.value;
|
|
655
554
|
input.value = '';
|
|
656
555
|
|
|
657
|
-
const msg = {
|
|
658
|
-
msg_id: `pending-${this.newMessageCount++}`,
|
|
556
|
+
const msg: SendMsgCmd = {
|
|
557
|
+
// msg_id: `pending-${this.newMessageCount++}`,
|
|
659
558
|
type: 'send_msg',
|
|
660
|
-
text: text
|
|
661
|
-
time: new Date().toISOString()
|
|
559
|
+
text: text
|
|
560
|
+
// time: new Date().toISOString()
|
|
662
561
|
};
|
|
663
562
|
|
|
664
|
-
this.addMessage(msg);
|
|
665
|
-
this.insertGroups(this.groupMessages([msg.msg_id]), true);
|
|
666
563
|
this.sendSockMessage(msg);
|
|
667
|
-
this.hasPendingText = input.value.length > 0;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
private scrollToBottom() {
|
|
672
|
-
const scroll = this.shadowRoot.querySelector('.scroll');
|
|
673
|
-
if (scroll) {
|
|
674
|
-
scroll.scrollTop = scroll.scrollHeight;
|
|
675
|
-
this.hideBottomScroll = true;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
private renderMessageGroup(
|
|
680
|
-
msgIds: string[],
|
|
681
|
-
idx: number,
|
|
682
|
-
groups: string[][]
|
|
683
|
-
): TemplateResult {
|
|
684
|
-
const today = new Date();
|
|
685
|
-
let prevMsg;
|
|
686
|
-
if (idx > 0) {
|
|
687
|
-
const lastGroup = groups[idx - 1];
|
|
688
|
-
if (lastGroup && lastGroup.length > 0) {
|
|
689
|
-
prevMsg = this.msgMap.get(lastGroup[0]);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const currentMsg = this.msgMap.get(msgIds[msgIds.length - 1]);
|
|
694
|
-
let timeDisplay = null;
|
|
695
|
-
if (
|
|
696
|
-
prevMsg &&
|
|
697
|
-
!this.isSameGroup(prevMsg, currentMsg) &&
|
|
698
|
-
prevMsg.timeAsDate.getTime() - currentMsg.timeAsDate.getTime() >
|
|
699
|
-
BATCH_TIME_WINDOW
|
|
700
|
-
) {
|
|
701
|
-
const showDay =
|
|
702
|
-
!prevMsg ||
|
|
703
|
-
prevMsg.timeAsDate.getDate() !== currentMsg.timeAsDate.getDate();
|
|
704
|
-
if (showDay) {
|
|
705
|
-
timeDisplay = html`<div class="time">
|
|
706
|
-
${prevMsg.timeAsDate.toLocaleDateString(undefined, DAY_FORMAT)}
|
|
707
|
-
</div>`;
|
|
708
|
-
} else {
|
|
709
|
-
if (prevMsg.timeAsDate.getDate() !== today.getDate()) {
|
|
710
|
-
timeDisplay = html`<div class="time">
|
|
711
|
-
${prevMsg.timeAsDate.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
|
|
712
|
-
</div>`;
|
|
713
|
-
} else {
|
|
714
|
-
timeDisplay = html`<div class="time">
|
|
715
|
-
${prevMsg.timeAsDate.toLocaleTimeString(undefined, TIME_FORMAT)}
|
|
716
|
-
</div>`;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
564
|
|
|
721
|
-
|
|
722
|
-
const message = this.msgMap.get(msgIds[0]);
|
|
723
|
-
const incoming = !message.origin;
|
|
724
|
-
const avatar = message.user?.avatar;
|
|
725
|
-
const name = message.user?.name;
|
|
726
|
-
|
|
727
|
-
return html` <div
|
|
728
|
-
class="block ${incoming ? 'incoming' : 'outgoing'} ${idx === 0
|
|
729
|
-
? 'first'
|
|
730
|
-
: ''}"
|
|
731
|
-
title="${blockTime.toLocaleTimeString(undefined, VERBOSE_FORMAT)}"
|
|
732
|
-
>
|
|
733
|
-
<div class="row">
|
|
734
|
-
${!incoming
|
|
735
|
-
? html`
|
|
736
|
-
<div
|
|
737
|
-
class="avatar"
|
|
738
|
-
style="background: center / contain no-repeat url(${avatar ||
|
|
739
|
-
DEFAULT_AVATAR})"
|
|
740
|
-
></div>
|
|
741
|
-
`
|
|
742
|
-
: null}
|
|
565
|
+
const date = new Date();
|
|
743
566
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
${this.msgMap
|
|
751
|
-
.get(msgId)
|
|
752
|
-
.timeAsDate.toLocaleDateString(undefined, VERBOSE_FORMAT)}
|
|
753
|
-
</div-->`
|
|
754
|
-
)}
|
|
755
|
-
</div>
|
|
756
|
-
</div>
|
|
757
|
-
${timeDisplay}
|
|
758
|
-
</div>`;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
private handleScroll(event: any) {
|
|
762
|
-
const ele = event.target;
|
|
763
|
-
const top = ele.scrollHeight - ele.clientHeight;
|
|
764
|
-
const scroll = Math.round(top + ele.scrollTop);
|
|
765
|
-
const scrollPct = scroll / top;
|
|
766
|
-
|
|
767
|
-
this.hideTopScroll = scrollPct <= 0.01;
|
|
768
|
-
this.hideBottomScroll = scrollPct >= 0.99;
|
|
769
|
-
|
|
770
|
-
if (this.blockHistoryFetching) {
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
if (scrollPct < SCROLL_FETCH_BUFFER) {
|
|
775
|
-
this.fetchPreviousMessages();
|
|
567
|
+
this.chat.addMessages(
|
|
568
|
+
[{ type: MessageType.MsgIn, text, date }],
|
|
569
|
+
date,
|
|
570
|
+
true
|
|
571
|
+
);
|
|
572
|
+
this.hasPendingText = input.value.length > 0;
|
|
776
573
|
}
|
|
777
574
|
}
|
|
778
575
|
|
|
@@ -789,13 +586,7 @@ export class WebChat extends LitElement {
|
|
|
789
586
|
|
|
790
587
|
public render(): TemplateResult {
|
|
791
588
|
return html`
|
|
792
|
-
<div
|
|
793
|
-
class="chat ${this.status} ${this.hideTopScroll
|
|
794
|
-
? 'scroll-at-top'
|
|
795
|
-
: ''} ${this.hideBottomScroll ? 'scroll-at-bottom' : ''} ${this.open
|
|
796
|
-
? 'open'
|
|
797
|
-
: ''}"
|
|
798
|
-
>
|
|
589
|
+
<div class="chat ${this.status} ${this.open ? 'open' : ''}">
|
|
799
590
|
<div class="header">
|
|
800
591
|
<slot name="header">${this.urn ? this.urn : 'Chat'}</slot>
|
|
801
592
|
<temba-icon
|
|
@@ -805,20 +596,11 @@ export class WebChat extends LitElement {
|
|
|
805
596
|
@click=${this.toggleChat}
|
|
806
597
|
></temba-icon>
|
|
807
598
|
</div>
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
html`${this.renderMessageGroup(msgGroup, idx, groups)}`
|
|
814
|
-
)
|
|
815
|
-
: null}
|
|
816
|
-
|
|
817
|
-
<temba-loading
|
|
818
|
-
class="${!this.fetching ? 'hidden' : ''}"
|
|
819
|
-
></temba-loading>
|
|
820
|
-
</div>
|
|
821
|
-
</div>
|
|
599
|
+
|
|
600
|
+
<temba-chat
|
|
601
|
+
@temba-scroll-threshold=${this.fetchPreviousMessages}
|
|
602
|
+
@temba-fetch-complete=${this.fetchComplete}
|
|
603
|
+
></temba-chat>
|
|
822
604
|
|
|
823
605
|
${this.status === ChatStatus.DISCONNECTED
|
|
824
606
|
? html`<div class="notice">
|
|
@@ -836,28 +618,29 @@ export class WebChat extends LitElement {
|
|
|
836
618
|
</div>`
|
|
837
619
|
: null}
|
|
838
620
|
${this.status === ChatStatus.CONNECTED
|
|
839
|
-
? html` <div
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
621
|
+
? html` <div class="border"></div>
|
|
622
|
+
<div
|
|
623
|
+
class="row input-panel ${this.hasPendingText ? 'pending' : ''}"
|
|
624
|
+
@click=${this.handleClickInputPanel}
|
|
625
|
+
>
|
|
626
|
+
<input
|
|
627
|
+
class="input ${this.status === ChatStatus.CONNECTED
|
|
628
|
+
? 'active'
|
|
629
|
+
: 'inactive'}"
|
|
630
|
+
type="text"
|
|
631
|
+
placeholder="Message.."
|
|
632
|
+
?disabled=${this.status !== ChatStatus.CONNECTED}
|
|
633
|
+
@keydown=${this.handleKeyUp}
|
|
634
|
+
/>
|
|
635
|
+
<temba-icon
|
|
636
|
+
tabindex="1"
|
|
637
|
+
class="send-icon"
|
|
638
|
+
name="send"
|
|
639
|
+
size="1"
|
|
640
|
+
clickable
|
|
641
|
+
@click=${this.sendPendingMessage}
|
|
642
|
+
></temba-icon>
|
|
643
|
+
</div>`
|
|
861
644
|
: null}
|
|
862
645
|
</div>
|
|
863
646
|
|