@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/demo/index.html +1 -1
  3. package/dist/temba-components.js +760 -1189
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/chat/Chat.js +714 -0
  6. package/out-tsc/src/chat/Chat.js.map +1 -0
  7. package/out-tsc/src/completion/helpers.js +1 -29
  8. package/out-tsc/src/completion/helpers.js.map +1 -1
  9. package/out-tsc/src/compose/Compose.js +6 -2
  10. package/out-tsc/src/compose/Compose.js.map +1 -1
  11. package/out-tsc/src/contacts/ContactChat.js +518 -54
  12. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  13. package/out-tsc/src/contacts/events.js +1 -998
  14. package/out-tsc/src/contacts/events.js.map +1 -1
  15. package/out-tsc/src/lightbox/Lightbox.js +4 -0
  16. package/out-tsc/src/lightbox/Lightbox.js.map +1 -1
  17. package/out-tsc/src/list/TembaMenu.js +0 -1
  18. package/out-tsc/src/list/TembaMenu.js.map +1 -1
  19. package/out-tsc/src/markdown.js +33 -0
  20. package/out-tsc/src/markdown.js.map +1 -0
  21. package/out-tsc/src/select/Select.js +6 -1
  22. package/out-tsc/src/select/Select.js.map +1 -1
  23. package/out-tsc/src/textinput/TextInput.js +1 -1
  24. package/out-tsc/src/textinput/TextInput.js.map +1 -1
  25. package/out-tsc/src/thumbnail/Thumbnail.js +128 -81
  26. package/out-tsc/src/thumbnail/Thumbnail.js.map +1 -1
  27. package/out-tsc/src/utils/index.js +9 -11
  28. package/out-tsc/src/utils/index.js.map +1 -1
  29. package/out-tsc/src/webchat/WebChat.js +109 -358
  30. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  31. package/out-tsc/src/webchat/index.js +17 -0
  32. package/out-tsc/src/webchat/index.js.map +1 -1
  33. package/out-tsc/temba-modules.js +2 -2
  34. package/out-tsc/temba-modules.js.map +1 -1
  35. package/out-tsc/temba-webchat.js +2 -0
  36. package/out-tsc/temba-webchat.js.map +1 -1
  37. package/out-tsc/test/temba-contact-chat.test.js +1 -0
  38. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  39. package/out-tsc/test/temba-lightbox.test.js +4 -4
  40. package/out-tsc/test/temba-lightbox.test.js.map +1 -1
  41. package/package.json +1 -1
  42. package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
  43. package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
  44. package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
  45. package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
  46. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text-and-attachments.png +0 -0
  47. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
  48. package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
  49. package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
  50. package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
  51. package/screenshots/truth/contacts/contact-active-default.png +0 -0
  52. package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
  53. package/screenshots/truth/contacts/contact-archived-hide-chatbox.png +0 -0
  54. package/screenshots/truth/contacts/contact-blocked-hide-chatbox.png +0 -0
  55. package/screenshots/truth/contacts/contact-stopped-hide-chatbox.png +0 -0
  56. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  57. package/screenshots/truth/lightbox/img.png +0 -0
  58. package/src/chat/Chat.ts +791 -0
  59. package/src/completion/helpers.ts +2 -40
  60. package/src/compose/Compose.ts +6 -2
  61. package/src/contacts/ContactChat.ts +609 -59
  62. package/src/contacts/events.ts +1 -1068
  63. package/src/lightbox/Lightbox.ts +5 -0
  64. package/src/list/TembaMenu.ts +0 -1
  65. package/src/markdown.ts +41 -0
  66. package/src/select/Select.ts +5 -1
  67. package/src/textinput/TextInput.ts +1 -1
  68. package/src/thumbnail/Thumbnail.ts +130 -81
  69. package/src/utils/index.ts +12 -13
  70. package/src/webchat/WebChat.ts +196 -413
  71. package/src/webchat/index.ts +23 -1
  72. package/static/css/temba-components.css +2 -0
  73. package/temba-modules.ts +2 -2
  74. package/temba-webchat.ts +2 -0
  75. package/test/temba-contact-chat.test.ts +1 -0
  76. package/test/temba-lightbox.test.ts +4 -4
  77. package/test-assets/contacts/history.json +1 -56
  78. package/out-tsc/src/contacts/ContactHistory.js +0 -691
  79. package/out-tsc/src/contacts/ContactHistory.js.map +0 -1
  80. package/out-tsc/test/temba-contact-history.test.js +0 -69
  81. package/out-tsc/test/temba-contact-history.test.js.map +0 -1
  82. package/src/contacts/ContactHistory.ts +0 -875
  83. package/test/temba-contact-history.test.ts +0 -107
@@ -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 Message {
14
- type: string;
15
- msg_id?: string;
16
- text?: string;
17
- chat_id?: string;
18
- origin?: string;
19
- time?: string;
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
- history?: Message[];
22
- timeAsDate?: Date;
23
- user?: User;
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
- // how long of a window to show time between batches
33
- const BATCH_TIME_WINDOW = 30 * 60 * 1000;
34
- const SCROLL_FETCH_BUFFER = 0.05;
35
- const MIN_FETCH_TIME = 250;
36
-
37
- const TIME_FORMAT = { hour: 'numeric', minute: '2-digit' } as any;
38
- const DAY_FORMAT = {
39
- weekday: undefined,
40
- year: 'numeric',
41
- month: 'short',
42
- day: 'numeric'
43
- } as any;
44
- const VERBOSE_FORMAT = {
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
- private msgMap = new Map<string, Message>();
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(message: Message) {
408
- console.log('MO', message);
409
- this.sock.send(JSON.stringify(message));
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 (event: CloseEvent) {
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 (event: Event) {
431
- console.log('Socket opened', event);
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 startChat = { type: 'start_chat' };
437
+ const cmd: StartChatCmd = { type: 'start_chat' };
435
438
  if (webChat.urn) {
436
- startChat['chat_id'] = webChat.urn;
439
+ cmd.chat_id = webChat.urn;
437
440
  }
438
- webChat.sendSockMessage(startChat);
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 Message;
444
- console.log('MT', msg);
445
-
446
+ const msg = JSON.parse(event.data) as SockMsg;
447
+ console.log('in', msg);
446
448
  if (msg.type === 'chat_started') {
447
- if (webChat.urn !== msg.chat_id) {
449
+ const response = msg as StartChatResponse;
450
+ if (webChat.urn !== response.chat_id) {
448
451
  webChat.messageGroups = [];
449
452
  }
450
- webChat.urn = msg.chat_id;
451
- setCookie('temba-chat-urn', msg.chat_id);
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
- webChat.oldestMessageDate = new Date(msg.time);
455
- webChat.urn = msg.chat_id;
457
+ const response = msg as ResumeChatResponse;
458
+ webChat.urn = response.chat_id;
456
459
  webChat.fetchPreviousMessages();
457
- } else if (msg.type === 'msg_out') {
458
- webChat.addMessage(msg);
459
- webChat.insertGroups(webChat.groupMessages([msg.msg_id]), true);
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
- private isSameGroup(msg1: Message, msg2: Message): boolean {
467
- if (msg1 && msg2) {
468
- return (
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
- private fetchPreviousMessages() {
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(getHistoryMsg);
488
+ this.sendSockMessage(cmd);
541
489
  }
542
490
  }
543
491
 
544
- private handleHistoryResponse(msg: Message) {
545
- const elapsed = new Date().getTime() - this.fetchRequested.getTime();
546
- window.setTimeout(
547
- () => {
548
- this.fetching = false;
549
- // block of historical messages
550
- const msgs = msg.history.reverse();
551
-
552
- // first add messages to the map
553
- const newMessages = [];
554
- for (const m of msgs) {
555
- if (this.addMessage(m)) {
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
- this.insertGroups(this.groupMessages(newMessages));
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 firstUpdated(
582
- changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
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
- const blockTime = new Date(this.msgMap.get(msgIds[msgIds.length - 1]).time);
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
- <div class="bubble">
745
- ${!incoming ? html`<div class="name">${name}</div>` : null}
746
- ${msgIds.map(
747
- (msgId) =>
748
- html`<div class="message">${this.msgMap.get(msgId).text}</div>
749
- <!--div style="font-size:10px">
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
- <div class="messages">
809
- <div class="scroll" @scroll=${this.handleScroll}>
810
- ${this.messageGroups
811
- ? this.messageGroups.map(
812
- (msgGroup, idx, groups) =>
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
- class="row input-panel ${this.hasPendingText ? 'pending' : ''}"
841
- @click=${this.handleClickInputPanel}
842
- >
843
- <input
844
- class="input ${this.status === ChatStatus.CONNECTED
845
- ? 'active'
846
- : 'inactive'}"
847
- type="text"
848
- placeholder="Message.."
849
- ?disabled=${this.status !== ChatStatus.CONNECTED}
850
- @keydown=${this.handleKeyUp}
851
- />
852
- <temba-icon
853
- tabindex="1"
854
- class="send-icon"
855
- name="send"
856
- size="1"
857
- clickable
858
- @click=${this.sendPendingMessage}
859
- ></temba-icon>
860
- </div>`
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