@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
@@ -1,12 +1,243 @@
1
- import { css, html, TemplateResult } from 'lit';
1
+ /* eslint-disable @typescript-eslint/no-this-alias */
2
+ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
3
  import { property } from 'lit/decorators.js';
3
4
  import { Contact, CustomEventType, Ticket } from '../interfaces';
4
- import { postJSON } from '../utils';
5
- import { ContactHistory } from './ContactHistory';
5
+ import { oxford, oxfordFn, postJSON } from '../utils';
6
6
  import { ContactStoreElement } from './ContactStoreElement';
7
7
  import { Compose } from '../compose/Compose';
8
+ import { fetchContactHistory, getDisplayName } from './helpers';
9
+ import {
10
+ AirtimeTransferredEvent,
11
+ CampaignFiredEvent,
12
+ ChannelEvent,
13
+ ContactEvent,
14
+ ContactGroupsEvent,
15
+ ContactHistoryPage,
16
+ ContactLanguageChangedEvent,
17
+ EmailSentEvent,
18
+ ErrorMessageEvent,
19
+ FlowEvent,
20
+ LabelsAddedEvent,
21
+ MsgEvent,
22
+ NameChangedEvent,
23
+ OptinRequestedEvent,
24
+ TicketEvent,
25
+ UpdateFieldEvent,
26
+ UpdateResultEvent,
27
+ URNsChangedEvent,
28
+ WebhookEvent
29
+ } from './events';
30
+ import { Chat, ChatEvent, MessageType } from '../chat/Chat';
31
+ import { getUserDisplay } from '../webchat';
32
+ import { DEFAULT_AVATAR } from '../webchat/assets';
33
+
34
+ export enum Events {
35
+ MESSAGE_CREATED = 'msg_created',
36
+ MESSAGE_RECEIVED = 'msg_received',
37
+ BROADCAST_CREATED = 'broadcast_created',
38
+ IVR_CREATED = 'ivr_created',
39
+ FLOW_ENTERED = 'flow_entered',
40
+
41
+ FLOW_EXITED = 'flow_exited',
42
+ RUN_RESULT_CHANGED = 'run_result_changed',
43
+ CONTACT_FIELD_CHANGED = 'contact_field_changed',
44
+ CONTACT_GROUPS_CHANGED = 'contact_groups_changed',
45
+ CONTACT_NAME_CHANGED = 'contact_name_changed',
46
+ CONTACT_URNS_CHANGED = 'contact_urns_changed',
47
+ CAMPAIGN_FIRED = 'campaign_fired',
48
+ CHANNEL_EVENT = 'channel_event',
49
+ CONTACT_LANGUAGE_CHANGED = 'contact_language_changed',
50
+ WEBHOOK_CALLED = 'webhook_called',
51
+ AIRTIME_TRANSFERRED = 'airtime_transferred',
52
+ CALL_STARTED = 'call_started',
53
+ EMAIL_SENT = 'email_sent',
54
+ INPUT_LABELS_ADDED = 'input_labels_added',
55
+ NOTE_CREATED = 'note_created',
56
+ TICKET_ASSIGNED = 'ticket_assigned',
57
+ TICKET_NOTE_ADDED = 'ticket_note_added',
58
+ TICKET_CLOSED = 'ticket_closed',
59
+ TICKET_OPENED = 'ticket_opened',
60
+ TICKET_REOPENED = 'ticket_reopened',
61
+ OPTIN_REQUESTED = 'optin_requested',
62
+ ERROR = 'error',
63
+ FAILURE = 'failure'
64
+ }
65
+
66
+ const renderInfoList = (singular: string, plural: string, items: any[]) => {
67
+ if (items.length === 1) {
68
+ return `${singular} **${items[0].name}**`;
69
+ } else {
70
+ const list = items.map((item) => `**${item.name}**`);
71
+ if (list.length === 2) {
72
+ return `${plural} ${list.join(' and ')}`;
73
+ } else {
74
+ const last = list.pop();
75
+ return `${plural} ${list.join(', ')}, and ${last}`;
76
+ }
77
+ }
78
+ };
79
+
80
+ const toTitleCase = (str: string) => {
81
+ return str.charAt(0).toUpperCase() + str.slice(1);
82
+ };
83
+
84
+ const renderChannelEvent = (event: ChannelEvent): string => {
85
+ if (event.event.type === 'mt_miss') {
86
+ return 'Missed outgoing call';
87
+ } else if (event.event.type === 'mo_miss') {
88
+ return 'Missed incoming call';
89
+ } else if (event.event.type === 'new_conversation') {
90
+ return 'Started conversation';
91
+ } else if (event.channel_event_type === 'welcome_message') {
92
+ return 'Welcome Message Sent';
93
+ } else if (event.event.type === 'referral') {
94
+ return 'Referred';
95
+ } else if (event.event.type === 'follow') {
96
+ return 'Followed';
97
+ } else if (event.event.type === 'stop_contact') {
98
+ return 'Stopped';
99
+ } else if (event.event.type === 'mt_call') {
100
+ return 'Outgoing Phone Call';
101
+ } else if (event.event.type == 'mo_call') {
102
+ return 'Incoming Phone call';
103
+ } else if (event.event.type == 'optin') {
104
+ return `Opted in to **${event.event.optin?.name}**`;
105
+ } else if (event.event.type == 'optout') {
106
+ return `Opted out of **${event.event.optin?.name}**`;
107
+ }
108
+ };
109
+
110
+ const renderFlowEvent = (event: FlowEvent): string => {
111
+ let verb = 'Interrupted';
112
+ if (event.status !== 'I') {
113
+ if (event.type === Events.FLOW_ENTERED) {
114
+ verb = 'Started';
115
+ } else {
116
+ verb = 'Completed';
117
+ }
118
+ }
119
+ return `${verb} [**${event.flow.name}**](/flow/editor/${event.flow.uuid}/)`;
120
+ };
121
+
122
+ const renderResultEvent = (event: UpdateResultEvent): string => {
123
+ if (!event.name.startsWith('_') && event.value) {
124
+ return `Updated flow result **${event.name}** to **${event.value}**`;
125
+ }
126
+ };
127
+
128
+ const renderUpdateEvent = (event: UpdateFieldEvent): string => {
129
+ return event.value
130
+ ? `Updated **${event.field.name}** to **${event.value.text}**`
131
+ : `Cleared **${event.field.name}**`;
132
+ };
133
+
134
+ const renderNameChanged = (event: NameChangedEvent): string => {
135
+ return `Updated **Contact Name** to **${event.name}**`;
136
+ };
137
+
138
+ const renderContactURNsChanged = (event: URNsChangedEvent): string => {
139
+ return `Updated **URNs** to ${oxfordFn(
140
+ event.urns,
141
+ (urn: string) => `**${urn.split(':')[1].split('?')[0]}**`
142
+ )}`;
143
+ };
144
+
145
+ const renderEmailSent = (event: EmailSentEvent): string => {
146
+ return `Email sent to **${oxford(event.to, 'and')}** with subject **${
147
+ event.subject
148
+ }**`;
149
+ };
150
+
151
+ const renderLabelsAdded = (event: LabelsAddedEvent): string => {
152
+ return `Applied ${renderInfoList('label', 'labels', event.labels)}`;
153
+ };
154
+
155
+ export const renderTicketAction = (
156
+ event: TicketEvent,
157
+ action: string
158
+ ): string => {
159
+ if (event.created_by) {
160
+ return `**${getUserDisplay(
161
+ event.created_by
162
+ )}** ${action} a **[ticket](/ticket/all/closed/${event.ticket.uuid}/)**`;
163
+ }
164
+ return `A **[ticket](/ticket/all/closed/${event.ticket.uuid}/)** was **${action}**`;
165
+ };
166
+
167
+ export const renderTicketAssigned = (event: TicketEvent): string => {
168
+ return event.assignee
169
+ ? event.assignee.id === event.created_by.id
170
+ ? `**${getDisplayName(event.created_by)}** took this ticket`
171
+ : `${getDisplayName(
172
+ event.created_by
173
+ )} assigned this ticket to **${getDisplayName(event.assignee)}**`
174
+ : `**${getDisplayName(event.created_by)}** unassigned this ticket`;
175
+ };
176
+
177
+ export const renderContactGroupsEvent = (event: ContactGroupsEvent): string => {
178
+ const groupsEvent = event as ContactGroupsEvent;
179
+ if (groupsEvent.groups_added) {
180
+ return renderInfoList(
181
+ 'Added to group',
182
+ 'Added to groups',
183
+ groupsEvent.groups_added
184
+ );
185
+ } else if (groupsEvent.groups_removed) {
186
+ return renderInfoList(
187
+ 'Removed from group',
188
+ 'Removed from groups',
189
+ groupsEvent.groups_removed
190
+ );
191
+ }
192
+ };
193
+
194
+ export const renderCampaignFiredEvent = (event: CampaignFiredEvent): string => {
195
+ return `Campaign ${event.campaign.name}
196
+ ${event.fired_result === 'S' ? 'skipped' : 'triggered'}
197
+ ${event.campaign_event.offset_display}
198
+ ${event.campaign_event.relative_to.name}`;
199
+ };
200
+
201
+ export const renderTicketOpened = (event: TicketEvent): string => {
202
+ return `${event.ticket.topic.name} ticket was opened`;
203
+ };
204
+
205
+ export const renderErrorMessage = (event: ErrorMessageEvent): string => {
206
+ return `${event.text} ${
207
+ event.type === Events.FAILURE
208
+ ? `Run ended prematurely, check the flow design`
209
+ : null
210
+ }`;
211
+ };
212
+
213
+ export const renderWebhookEvent = (event: WebhookEvent): string => {
214
+ return event.status === 'success'
215
+ ? `Successfully called ${event.url}`
216
+ : `Failed to call ${event.url}`;
217
+ };
218
+
219
+ export const renderAirtimeTransferredEvent = (
220
+ event: AirtimeTransferredEvent
221
+ ): string => {
222
+ if (parseFloat(event.actual_amount) === 0) {
223
+ return `Airtime transfer failed`;
224
+ }
225
+ return `Transferred **${event.actual_amount}** ${event.currency} of airtime`;
226
+ };
227
+
228
+ export const renderCallStartedEvent = (): string => {
229
+ return `Call Started`;
230
+ };
231
+
232
+ export const renderContactLanguageChangedEvent = (
233
+ event: ContactLanguageChangedEvent
234
+ ): string => {
235
+ return `Language updated to **${event.language}**`;
236
+ };
8
237
 
9
- const DEFAULT_REFRESH = 10000;
238
+ export const renderOptinRequested = (event: OptinRequestedEvent): string => {
239
+ return `Requested opt-in for ${event.optin.name}`;
240
+ };
10
241
 
11
242
  export class ContactChat extends ContactStoreElement {
12
243
  public static get styles() {
@@ -38,7 +269,7 @@ export class ContactChat extends ContactStoreElement {
38
269
  }
39
270
 
40
271
  .chatbox {
41
- box-shadow: 0px -5px 1rem 0rem rgba(0, 0, 0, 0.07);
272
+ background: #fff;
42
273
  display: flex;
43
274
  flex-direction: column;
44
275
  --textarea-min-height: 1em;
@@ -46,10 +277,6 @@ export class ContactChat extends ContactStoreElement {
46
277
  --widget-box-shadow-focused: none;
47
278
  }
48
279
 
49
- .chatbox:focus-within {
50
- --textarea-height: 4em;
51
- }
52
-
53
280
  .chatbox.full {
54
281
  border-bottom-right-radius: 0 !important;
55
282
  }
@@ -84,6 +311,11 @@ export class ContactChat extends ContactStoreElement {
84
311
  --color-focus: transparent;
85
312
  --color-widget-bg-focused: transparent;
86
313
  }
314
+
315
+ .border {
316
+ border-top: 1px solid #f1f1f1;
317
+ margin: 0 1em;
318
+ }
87
319
  `;
88
320
  }
89
321
 
@@ -99,9 +331,6 @@ export class ContactChat extends ContactStoreElement {
99
331
  @property({ type: Boolean })
100
332
  showDetails = true;
101
333
 
102
- @property({ type: Boolean })
103
- monitor = false;
104
-
105
334
  @property({ type: Object })
106
335
  currentTicket: Ticket = null;
107
336
 
@@ -111,47 +340,40 @@ export class ContactChat extends ContactStoreElement {
111
340
  @property({ type: String })
112
341
  agent = '';
113
342
 
343
+ @property({ type: Boolean })
344
+ blockFetching = false;
345
+
346
+ @property({ type: String })
347
+ avatar = DEFAULT_AVATAR;
348
+
114
349
  // http promise to monitor for completeness
115
350
  public httpComplete: Promise<void>;
351
+ private chat: Chat;
352
+
353
+ ticket = null;
354
+ lastEventTime = null;
355
+ newestEventTime = null;
356
+ refreshId = null;
357
+ polling = false;
116
358
 
117
359
  constructor() {
118
360
  super();
119
361
  }
120
362
 
121
- refreshInterval = null;
363
+ public firstUpdated(
364
+ changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
365
+ ): void {
366
+ super.firstUpdated(changed);
367
+ }
122
368
 
123
369
  public connectedCallback() {
124
370
  super.connectedCallback();
125
- if (this.monitor) {
126
- this.refreshInterval = setInterval(() => {
127
- if (this.currentTicket && this.currentTicket.closed_on) {
128
- return;
129
- }
130
- this.refresh();
131
- }, DEFAULT_REFRESH);
132
- }
371
+ this.chat = this.shadowRoot.querySelector('temba-chat');
133
372
  }
134
373
 
135
374
  public disconnectedCallback() {
136
- if (this.refreshInterval) {
137
- clearInterval(this.refreshInterval);
138
- }
139
- }
140
-
141
- public getContactHistory(): ContactHistory {
142
- return this.shadowRoot.querySelector(
143
- 'temba-contact-history'
144
- ) as ContactHistory;
145
- }
146
-
147
- public refresh(scrollToBottom = false): void {
148
- const contactHistory = this.getContactHistory();
149
- if (contactHistory) {
150
- if (scrollToBottom) {
151
- contactHistory.scrollToBottom();
152
- }
153
- contactHistory.refresh();
154
- // super.refresh();
375
+ if (this.refreshId) {
376
+ clearInterval(this.refreshId);
155
377
  }
156
378
  }
157
379
 
@@ -165,6 +387,25 @@ export class ContactChat extends ContactStoreElement {
165
387
  ) {
166
388
  this.currentContact = this.data;
167
389
  }
390
+
391
+ if (changedProperties.has('currentContact')) {
392
+ this.chat = this.shadowRoot.querySelector('temba-chat');
393
+ this.reset();
394
+ this.fetchPreviousMessages();
395
+ }
396
+ }
397
+
398
+ private reset() {
399
+ this.blockFetching = false;
400
+ this.ticket = null;
401
+ this.lastEventTime = null;
402
+ this.newestEventTime = null;
403
+ this.refreshId = null;
404
+ this.polling = false;
405
+ }
406
+
407
+ public refresh() {
408
+ this.checkForNewMessages();
168
409
  }
169
410
 
170
411
  private handleSend(evt: CustomEvent) {
@@ -196,8 +437,8 @@ export class ContactChat extends ContactStoreElement {
196
437
  postJSON(`/api/v2/messages.json`, payload)
197
438
  .then((response) => {
198
439
  if (response.status < 400) {
440
+ this.checkForNewMessages();
199
441
  compose.reset();
200
- this.refresh(true);
201
442
  this.fireCustomEvent(CustomEventType.MessageSent, { msg: payload });
202
443
  } else if (response.status < 500) {
203
444
  if (
@@ -250,15 +491,322 @@ export class ContactChat extends ContactStoreElement {
250
491
  return html`${contactHistoryAndChatbox}`;
251
492
  }
252
493
 
494
+ private getEndpoint() {
495
+ return `/contact/history/${this.currentContact.uuid}/?_format=json`;
496
+ }
497
+
498
+ private scheduleRefresh() {
499
+ // knock five seconds off the newest event time so we are
500
+ // a little more aggressive about refreshing short term
501
+ let window = new Date().getTime() - this.newestEventTime / 1000 - 5000;
502
+
503
+ if (this.refreshId) {
504
+ clearTimeout(this.refreshId);
505
+ this.refreshId = null;
506
+ }
507
+
508
+ // wait no longer than 15 seconds
509
+ window = Math.min(window, 15000);
510
+
511
+ // wait at least 2 seconds
512
+ window = Math.max(window, 2000);
513
+
514
+ this.refreshId = setTimeout(() => {
515
+ this.checkForNewMessages();
516
+ }, window);
517
+ }
518
+
519
+ public getEventMessage(event: ContactEvent): ChatEvent {
520
+ let message = null;
521
+ switch (event.type) {
522
+ case Events.ERROR:
523
+ case Events.FAILURE:
524
+ message = {
525
+ type: MessageType.Inline,
526
+ text: `Error during flow: ${toTitleCase(
527
+ (event as ErrorMessageEvent).text
528
+ )}`
529
+ };
530
+ break;
531
+ case Events.TICKET_OPENED:
532
+ message = {
533
+ type: MessageType.Inline,
534
+ text: renderTicketAction(event as TicketEvent, 'opened')
535
+ };
536
+ break;
537
+ case Events.TICKET_ASSIGNED:
538
+ message = {
539
+ type: MessageType.Inline,
540
+ text: renderTicketAssigned(event as TicketEvent)
541
+ };
542
+ break;
543
+ case Events.TICKET_REOPENED:
544
+ message = {
545
+ type: MessageType.Inline,
546
+ text: renderTicketAction(event as TicketEvent, 'reopened')
547
+ };
548
+ break;
549
+ case Events.TICKET_CLOSED:
550
+ message = {
551
+ type: MessageType.Inline,
552
+ text: renderTicketAction(event as TicketEvent, 'closed')
553
+ };
554
+ break;
555
+ case Events.FLOW_ENTERED:
556
+ case Events.FLOW_EXITED:
557
+ message = {
558
+ type: MessageType.Inline,
559
+ text: renderFlowEvent(event as FlowEvent)
560
+ };
561
+ break;
562
+ case Events.RUN_RESULT_CHANGED:
563
+ message = {
564
+ type: MessageType.Inline,
565
+ text: renderResultEvent(event as UpdateResultEvent)
566
+ };
567
+ break;
568
+ case Events.CONTACT_FIELD_CHANGED:
569
+ message = {
570
+ type: MessageType.Inline,
571
+ text: renderUpdateEvent(event as UpdateFieldEvent)
572
+ };
573
+ break;
574
+ case Events.CONTACT_NAME_CHANGED:
575
+ message = {
576
+ type: MessageType.Inline,
577
+ text: renderNameChanged(event as NameChangedEvent)
578
+ };
579
+ break;
580
+ case Events.CONTACT_URNS_CHANGED:
581
+ message = {
582
+ type: MessageType.Inline,
583
+ text: renderContactURNsChanged(event as URNsChangedEvent)
584
+ };
585
+ break;
586
+ case Events.EMAIL_SENT:
587
+ message = {
588
+ type: MessageType.Inline,
589
+ text: renderEmailSent(event as EmailSentEvent)
590
+ };
591
+ break;
592
+ case Events.INPUT_LABELS_ADDED:
593
+ message = {
594
+ type: MessageType.Inline,
595
+ text: renderLabelsAdded(event as LabelsAddedEvent)
596
+ };
597
+ break;
598
+ case Events.CONTACT_GROUPS_CHANGED:
599
+ message = {
600
+ type: MessageType.Inline,
601
+ text: renderContactGroupsEvent(event as ContactGroupsEvent)
602
+ };
603
+ break;
604
+ case Events.WEBHOOK_CALLED:
605
+ message = {
606
+ type: MessageType.Inline,
607
+ text: renderWebhookEvent(event as WebhookEvent)
608
+ };
609
+ break;
610
+ case Events.AIRTIME_TRANSFERRED:
611
+ message = {
612
+ type: MessageType.Inline,
613
+ text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
614
+ };
615
+ break;
616
+ case Events.CALL_STARTED:
617
+ message = {
618
+ type: MessageType.Inline,
619
+ text: renderCallStartedEvent()
620
+ };
621
+ break;
622
+ case Events.CAMPAIGN_FIRED:
623
+ message = {
624
+ type: MessageType.Inline,
625
+ text: renderCampaignFiredEvent(event as CampaignFiredEvent)
626
+ };
627
+ break;
628
+ case Events.CHANNEL_EVENT:
629
+ message = {
630
+ type: MessageType.Inline,
631
+ text: renderChannelEvent(event as ChannelEvent)
632
+ };
633
+ break;
634
+ case Events.CONTACT_LANGUAGE_CHANGED:
635
+ message = {
636
+ type: MessageType.Inline,
637
+ text: renderContactLanguageChangedEvent(
638
+ event as ContactLanguageChangedEvent
639
+ )
640
+ };
641
+ break;
642
+ case Events.OPTIN_REQUESTED:
643
+ message = {
644
+ type: MessageType.Inline,
645
+ text: renderOptinRequested(event as OptinRequestedEvent)
646
+ };
647
+ break;
648
+ }
649
+
650
+ message.date = new Date(event.created_on);
651
+ return message;
652
+ }
653
+
654
+ private getUserForEvent(event: MsgEvent | TicketEvent) {
655
+ let user = null;
656
+ if (event.created_by) {
657
+ const storeUser = this.store.getUser(event.created_by.email);
658
+ if (storeUser) {
659
+ user = {
660
+ email: event.created_by.email,
661
+ name: [storeUser.first_name, storeUser.last_name].join(' '),
662
+ avatar: storeUser.avatar
663
+ };
664
+ }
665
+ } else if (event.type === 'msg_received') {
666
+ user = {
667
+ name: this.currentContact.name
668
+ };
669
+ }
670
+ return user;
671
+ }
672
+
673
+ private createMessages(page: ContactHistoryPage): ChatEvent[] {
674
+ let messages = page.events.map((event) => {
675
+ const ts = new Date(event.created_on).getTime() * 1000;
676
+ if (ts > this.newestEventTime) {
677
+ this.newestEventTime = ts;
678
+ }
679
+
680
+ if (event.type === 'ticket_note_added') {
681
+ const ticketEvent = event as TicketEvent;
682
+ return {
683
+ type: MessageType.Note,
684
+ id: event.created_on + event.type,
685
+ user: this.getUserForEvent(ticketEvent),
686
+ date: new Date(ticketEvent.created_on),
687
+ text: ticketEvent.note
688
+ };
689
+ }
690
+
691
+ if (event.type === 'msg_created' || event.type === 'msg_received') {
692
+ const msgEvent = event as MsgEvent;
693
+ return {
694
+ type: msgEvent.type === 'msg_created' ? 'msg_out' : 'msg_in',
695
+ id: msgEvent.msg.id + '',
696
+ user: this.getUserForEvent(msgEvent),
697
+ date: new Date(msgEvent.created_on),
698
+ attachments: msgEvent.msg.attachments,
699
+ text: msgEvent.msg.text,
700
+ sendError: msgEvent.status === 'E' || msgEvent.status === 'F',
701
+ popup: html`<div
702
+ style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
703
+ >
704
+ <div style="justify-content:left;text-align:left">
705
+ <temba-date
706
+ value=${msgEvent.created_on}
707
+ display="duration"
708
+ ></temba-date>
709
+
710
+ ${msgEvent.failed_reason_display
711
+ ? html`
712
+ <div
713
+ style="margin-top:0.2em;margin-right: 0.5em;min-width:10em;max-width:15em;color:var(--color-error);font-size:0.9em"
714
+ >
715
+ ${msgEvent.failed_reason_display}
716
+ </div>
717
+ `
718
+ : null}
719
+ </div>
720
+ ${msgEvent.logs_url
721
+ ? html`<a style="margin-left:0.5em" href="${msgEvent.logs_url}"
722
+ ><temba-icon name="log"></temba-icon
723
+ ></a>`
724
+ : null}
725
+ </div> `
726
+ };
727
+ } else {
728
+ return this.getEventMessage(event);
729
+ }
730
+ });
731
+
732
+ // remove any messages we don't recognize
733
+ messages = messages.filter((msg) => !!msg);
734
+ return messages as ChatEvent[];
735
+ }
736
+
737
+ private checkForNewMessages() {
738
+ // we are already working on it
739
+ if (this.polling) {
740
+ return;
741
+ }
742
+
743
+ const chat = this.chat;
744
+ const contactChat = this;
745
+ if (this.currentContact && this.newestEventTime) {
746
+ this.polling = true;
747
+ const endpoint = this.getEndpoint();
748
+
749
+ fetchContactHistory(
750
+ false,
751
+ endpoint,
752
+ this.currentTicket?.uuid,
753
+ null,
754
+ this.newestEventTime
755
+ ).then((page: ContactHistoryPage) => {
756
+ this.lastEventTime = page.next_before;
757
+ const messages = this.createMessages(page);
758
+ if (messages.length === 0) {
759
+ contactChat.blockFetching = true;
760
+ }
761
+ messages.reverse();
762
+ chat.addMessages(messages, null, true);
763
+ this.polling = false;
764
+ this.scheduleRefresh();
765
+ });
766
+ }
767
+ }
768
+
769
+ private fetchPreviousMessages() {
770
+ const chat = this.chat;
771
+ const contactChat = this;
772
+
773
+ if (!chat || chat.fetching || contactChat.blockFetching) {
774
+ return;
775
+ }
776
+
777
+ chat.fetching = true;
778
+ if (this.currentContact) {
779
+ const endpoint = this.getEndpoint();
780
+ fetchContactHistory(
781
+ false,
782
+ endpoint,
783
+ this.currentTicket?.uuid,
784
+ this.lastEventTime
785
+ ).then((page: ContactHistoryPage) => {
786
+ this.lastEventTime = page.next_before;
787
+ const messages = this.createMessages(page);
788
+ messages.reverse();
789
+
790
+ if (messages.length === 0) {
791
+ contactChat.blockFetching = true;
792
+ }
793
+ chat.addMessages(messages);
794
+ this.scheduleRefresh();
795
+ });
796
+ }
797
+ }
798
+
799
+ private fetchComplete() {
800
+ this.chat.fetching = false;
801
+ }
802
+
253
803
  private getTembaContactHistory(): TemplateResult {
254
- return html` <temba-contact-history
255
- .uuid=${this.currentContact.uuid}
256
- .contact=${this.currentContact}
257
- .ticket=${this.currentTicket ? this.currentTicket.uuid : null}
258
- .endDate=${this.currentTicket ? this.currentTicket.closed_on : null}
259
- .agent=${this.agent}
260
- >
261
- </temba-contact-history>`;
804
+ return html`<temba-chat
805
+ @temba-scroll-threshold=${this.fetchPreviousMessages}
806
+ @temba-fetch-complete=${this.fetchComplete}
807
+ avatar=${this.avatar}
808
+ agent
809
+ ></temba-chat>`;
262
810
  }
263
811
 
264
812
  private getTembaChatbox(): TemplateResult {
@@ -286,15 +834,17 @@ export class ContactChat extends ContactStoreElement {
286
834
  }
287
835
 
288
836
  private getChatbox(): TemplateResult {
289
- return html`<div class="chatbox">
290
- <temba-compose
291
- chatbox
292
- attachments
293
- counter
294
- button
295
- @temba-button-clicked=${this.handleSend.bind(this)}
296
- >
297
- </temba-compose>
298
- </div>`;
837
+ return html`<div class="border"></div>
838
+ <div class="chatbox">
839
+ <temba-compose
840
+ chatbox
841
+ attachments
842
+ counter
843
+ button
844
+ autogrow
845
+ @temba-button-clicked=${this.handleSend.bind(this)}
846
+ >
847
+ </temba-compose>
848
+ </div>`;
299
849
  }
300
850
  }