@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,875 +0,0 @@
1
- import { css } from 'lit';
2
- import { property } from 'lit/decorators.js';
3
- import { html, TemplateResult } from 'lit-html';
4
- import { Contact, CustomEventType, Ticket } from '../interfaces';
5
- import { RapidElement } from '../RapidElement';
6
- import { Asset, getAssets, getClasses, postJSON, throttle } from '../utils';
7
-
8
- import {
9
- AirtimeTransferredEvent,
10
- CampaignFiredEvent,
11
- ChannelEvent,
12
- ContactEvent,
13
- ContactGroupsEvent,
14
- ContactHistoryPage,
15
- ContactLanguageChangedEvent,
16
- EmailSentEvent,
17
- ErrorMessageEvent,
18
- EventGroup,
19
- Events,
20
- FlowEvent,
21
- getEventGroupType,
22
- getEventStyles,
23
- LabelsAddedEvent,
24
- MsgEvent,
25
- NameChangedEvent,
26
- OptinRequestedEvent,
27
- renderAirtimeTransferredEvent,
28
- renderCallStartedEvent,
29
- renderCampaignFiredEvent,
30
- renderChannelEvent,
31
- renderContactGroupsEvent,
32
- renderContactLanguageChangedEvent,
33
- renderContactURNsChanged,
34
- renderEmailSent,
35
- renderErrorMessage,
36
- renderFlowEvent,
37
- renderLabelsAdded,
38
- renderMsgEvent,
39
- renderNameChanged,
40
- renderNoteCreated,
41
- renderOptinRequested,
42
- renderResultEvent,
43
- renderTicketAction,
44
- renderTicketAssigned,
45
- renderUpdateEvent,
46
- renderWebhookEvent,
47
- TicketEvent,
48
- UpdateFieldEvent,
49
- UpdateResultEvent,
50
- URNsChangedEvent,
51
- WebhookEvent
52
- } from './events';
53
- import {
54
- fetchContactHistory,
55
- MAX_CHAT_REFRESH,
56
- MIN_CHAT_REFRESH,
57
- SCROLL_THRESHOLD
58
- } from './helpers';
59
- import { Lightbox } from '../lightbox/Lightbox';
60
- import { Store } from '../store/Store';
61
-
62
- // when images load, make sure we are on the bottom of the scroll window if necessary
63
- export const loadHandler = function (event) {
64
- const target = event.target as HTMLElement;
65
- const events = this.host.getEventsPane();
66
- if (target.tagName == 'IMG') {
67
- if (!this.host.showMessageAlert) {
68
- if (
69
- events.scrollTop > target.offsetTop - 1000 &&
70
- target.offsetTop > events.scrollHeight - 500
71
- ) {
72
- this.host.scrollToBottom();
73
- }
74
- }
75
- }
76
- };
77
-
78
- export class ContactHistory extends RapidElement {
79
- public httpComplete: Promise<void | ContactHistoryPage>;
80
- private store: Store;
81
-
82
- public constructor() {
83
- super();
84
- }
85
-
86
- connectedCallback() {
87
- super.connectedCallback();
88
- this.shadowRoot.addEventListener('load', loadHandler, true);
89
- this.store = document.querySelector('temba-store') as Store;
90
- }
91
-
92
- disconnectedCallback() {
93
- super.disconnectedCallback();
94
- this.shadowRoot.removeEventListener('load', loadHandler, true);
95
- }
96
-
97
- private getTicketForEvent(event: TicketEvent) {
98
- return this.getTicket((event as TicketEvent).ticket.uuid);
99
- }
100
-
101
- private getTicket(uuid: string) {
102
- return (this.tickets || []).find((ticket) => ticket.uuid === uuid);
103
- }
104
-
105
- static get styles() {
106
- return css`
107
- ${getEventStyles()}
108
-
109
- .wrapper {
110
- border: 0px solid green;
111
- display: flex;
112
- flex-direction: column;
113
- align-items: items-stretch;
114
- flex-grow: 1;
115
- min-height: 0;
116
- }
117
-
118
- .events {
119
- overflow-y: scroll;
120
- overflow-x: hidden;
121
- background: #fff;
122
- display: flex;
123
- flex-direction: column;
124
- flex-grow: 1;
125
- min-height: 0;
126
- padding-top: 3em;
127
- padding-bottom: 1em;
128
- }
129
-
130
- temba-loading {
131
- align-self: center;
132
- margin-top: 0.025em;
133
- position: absolute;
134
- z-index: 250;
135
- padding-top: 1em;
136
- }
137
-
138
- .new-messages-container {
139
- display: flex;
140
- z-index: 1;
141
- background: pink;
142
- margin-bottom: 0px;
143
- }
144
-
145
- .new-messages {
146
- pointer-events: none;
147
- margin: 0 auto;
148
- margin-top: 0em;
149
- margin-bottom: -2.5em;
150
- padding: 0.25em 1em;
151
- border-radius: var(--curvature);
152
- background: var(--color-primary-dark);
153
- color: var(--color-text-light);
154
- opacity: 0;
155
- cursor: pointer;
156
- transition: all var(--transition-speed) ease-in-out;
157
- box-shadow: rgb(0 0 0 / 15%) 0px 3px 3px 0px;
158
- }
159
-
160
- .new-messages.expanded {
161
- margin-top: -2.5em;
162
- margin-bottom: 0.5em;
163
- pointer-events: auto;
164
- opacity: 1;
165
- pointer: cursor;
166
- }
167
-
168
- .scroll-title {
169
- display: flex;
170
- flex-direction: column;
171
- z-index: 2;
172
- border-top-left-radius: var(--curvature);
173
- overflow: hidden;
174
- box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.15);
175
- background: rgb(240, 240, 240);
176
- padding: 1em 1.2em;
177
- font-size: 1.2em;
178
- font-weight: 400;
179
- }
180
-
181
- .attachment img {
182
- cursor: pointer;
183
- }
184
- `;
185
- }
186
-
187
- @property({ type: Object })
188
- contact: Contact;
189
-
190
- @property({ type: String })
191
- uuid: string;
192
-
193
- @property({ type: String })
194
- agent: string;
195
-
196
- @property({ type: Array })
197
- eventGroups: EventGroup[] = [];
198
-
199
- @property({ type: Boolean })
200
- refreshing = false;
201
-
202
- @property({ type: Boolean })
203
- fetching = false;
204
-
205
- @property({ type: Boolean })
206
- complete = false;
207
-
208
- @property({ type: String })
209
- endpoint: string;
210
-
211
- @property({ type: Boolean })
212
- debug = false;
213
-
214
- @property({ type: Boolean })
215
- showMessageAlert = false;
216
-
217
- @property({ attribute: false, type: Object })
218
- mostRecentEvent: ContactEvent;
219
-
220
- @property({ type: String })
221
- ticket: string = null;
222
-
223
- @property({ type: String })
224
- endDate: string = null;
225
-
226
- @property({ type: Array })
227
- tickets: Ticket[] = null;
228
-
229
- ticketEvents: { [uuid: string]: TicketEvent } = {};
230
-
231
- nextBefore: number;
232
- nextAfter: number;
233
- lastHeight = 0;
234
- lastRefreshAdded: number;
235
- refreshTimeout: any = null;
236
- empty = false;
237
-
238
- public firstUpdated(changedProperties: Map<string, any>) {
239
- super.firstUpdated(changedProperties);
240
- this.handleClose = this.handleClose.bind(this);
241
- }
242
-
243
- public updated(changedProperties: Map<string, any>) {
244
- super.updated(changedProperties);
245
-
246
- // fire an event if we get a new event
247
- if (
248
- changedProperties.has('mostRecentEvent') &&
249
- changedProperties.get('mostRecentEvent') &&
250
- this.mostRecentEvent
251
- ) {
252
- this.fireCustomEvent(CustomEventType.Refreshed);
253
- }
254
-
255
- if (changedProperties.has('endDate')) {
256
- if (this.refreshTimeout && this.endDate) {
257
- window.clearTimeout(this.refreshTimeout);
258
- }
259
- }
260
-
261
- // if we don't have an endpoint infer one
262
- if (changedProperties.has('uuid')) {
263
- if (this.uuid == null) {
264
- this.reset();
265
- } else {
266
- const endpoint = `/contact/history/${this.uuid}/?_format=json`;
267
-
268
- if (this.endpoint !== endpoint) {
269
- this.reset();
270
-
271
- if (this.endDate) {
272
- const before = new Date(this.endDate);
273
- this.nextBefore = before.getTime() * 1000 + 1000;
274
- }
275
-
276
- this.endpoint = endpoint;
277
- this.refreshTickets();
278
- }
279
- }
280
- }
281
-
282
- if (changedProperties.has('ticket')) {
283
- this.endpoint = null;
284
- this.requestUpdate('uuid');
285
- }
286
-
287
- if (
288
- changedProperties.has('refreshing') &&
289
- this.refreshing &&
290
- this.endpoint &&
291
- !this.endDate
292
- ) {
293
- const after = (this.getLastEventTime() - 1) * 1000;
294
- let forceOpen = false;
295
-
296
- fetchContactHistory(false, this.endpoint, this.ticket, null, after)
297
- .then((results: ContactHistoryPage) => {
298
- if (results.events && results.events.length > 0) {
299
- this.updateMostRecent(results.events[0]);
300
- }
301
-
302
- // keep track of any ticket events
303
- results.events.forEach((event: ContactEvent) => {
304
- if (event.type === Events.TICKET_OPENED) {
305
- const ticketEvent = event as TicketEvent;
306
- this.ticketEvents[ticketEvent.ticket.uuid] = ticketEvent;
307
- }
308
- });
309
-
310
- const fetchedEvents = results.events.reverse();
311
-
312
- // dedupe any events we get from the server
313
- // TODO: perhaps make this a little less crazy
314
- let removed = 0;
315
- this.eventGroups.forEach((g) => {
316
- const before = g.events.length;
317
- g.events = g.events.filter(
318
- (prev) =>
319
- !fetchedEvents.find((fetched) => {
320
- return (
321
- prev.created_on == fetched.created_on &&
322
- prev.type === fetched.type
323
- );
324
- })
325
- );
326
- removed += before - g.events.length;
327
- });
328
-
329
- this.lastRefreshAdded = fetchedEvents.length - removed;
330
-
331
- // reflow our most recent event group in case it merges with our new groups
332
- const previousGroups = [...this.eventGroups];
333
-
334
- if (this.eventGroups.length > 0) {
335
- const sliced = previousGroups.splice(
336
- previousGroups.length - 1,
337
- 1
338
- )[0];
339
-
340
- forceOpen = sliced.open;
341
- if (sliced.events.length > 0) {
342
- fetchedEvents.splice(0, 0, ...sliced.events);
343
- }
344
- }
345
-
346
- const grouped = this.getEventGroups(fetchedEvents);
347
- if (grouped.length) {
348
- if (forceOpen) {
349
- grouped[grouped.length - 1].open = forceOpen;
350
- }
351
- this.eventGroups = [...previousGroups, ...grouped].filter(
352
- (group) => group.events.length > 0
353
- );
354
- }
355
- this.refreshing = false;
356
- this.scheduleRefresh();
357
- })
358
- .catch(() => {
359
- this.refreshing = false;
360
- this.scheduleRefresh();
361
- });
362
- }
363
-
364
- if (changedProperties.has('fetching') && this.fetching) {
365
- if (!this.nextBefore) {
366
- this.nextBefore = new Date().getTime() * 1000 - 1000;
367
- }
368
-
369
- this.httpComplete = fetchContactHistory(
370
- this.empty,
371
- this.endpoint,
372
- this.ticket,
373
- this.nextBefore,
374
- this.nextAfter
375
- ).then((results: ContactHistoryPage) => {
376
- // see if we have a new event
377
- if (results.events && results.events.length > 0) {
378
- this.updateMostRecent(results.events[0]);
379
-
380
- // keep track of any ticket events
381
- results.events.forEach((event: ContactEvent) => {
382
- if (event.type === Events.TICKET_OPENED) {
383
- const ticketEvent = event as TicketEvent;
384
- this.ticketEvents[ticketEvent.ticket.uuid] = ticketEvent;
385
- }
386
- });
387
- }
388
-
389
- let forceOpen = false;
390
- const fetchedEvents = results.events ? results.events.reverse() : [];
391
-
392
- // reflow our last event group in case it merges with our new groups
393
- if (this.eventGroups.length > 0) {
394
- const sliced = this.eventGroups.splice(0, 1)[0];
395
- forceOpen = sliced.open;
396
- fetchedEvents.push(...sliced.events);
397
- }
398
-
399
- const grouped = this.getEventGroups(fetchedEvents);
400
- if (grouped.length) {
401
- if (forceOpen) {
402
- grouped[grouped.length - 1].open = forceOpen;
403
- }
404
-
405
- this.eventGroups = [...grouped, ...this.eventGroups];
406
- }
407
-
408
- if (results.next_before === this.nextBefore) {
409
- this.complete = true;
410
- }
411
-
412
- this.nextBefore = results.next_before;
413
- this.nextAfter = results.next_after;
414
- this.fetching = false;
415
- this.empty = false;
416
- });
417
- }
418
-
419
- if (changedProperties.has('refreshing') && !this.refreshing) {
420
- if (this.lastRefreshAdded > 0) {
421
- const events = this.getEventsPane();
422
-
423
- // if we are near the bottom, push us to the bottom to show new stuff
424
- if (this.lastHeight > 0) {
425
- const addedHeight = events.scrollHeight - this.lastHeight;
426
-
427
- const distanceFromBottom =
428
- events.scrollHeight -
429
- events.scrollTop -
430
- addedHeight -
431
- events.clientHeight;
432
-
433
- if (distanceFromBottom < 500) {
434
- this.scrollToBottom();
435
- } else {
436
- this.showMessageAlert = true;
437
- }
438
- }
439
-
440
- if (this.eventGroups.length > 0) {
441
- this.lastHeight = events.scrollHeight;
442
- }
443
- }
444
- }
445
-
446
- if (
447
- changedProperties.has('fetching') &&
448
- !this.fetching &&
449
- changedProperties.get('fetching') !== undefined
450
- ) {
451
- const events = this.getEventsPane();
452
-
453
- if (this.lastHeight && events.scrollHeight > this.lastHeight) {
454
- const scrollTop =
455
- events.scrollTop + events.scrollHeight - this.lastHeight;
456
- events.scrollTop = scrollTop;
457
- }
458
-
459
- // scroll to the bottom if it's our first fetch
460
- if (!this.lastHeight) {
461
- this.scrollToBottom();
462
- }
463
-
464
- // don't record our scroll height until we have history
465
- if (this.eventGroups.length > 0) {
466
- this.lastHeight = events.scrollHeight;
467
- }
468
- }
469
-
470
- if (changedProperties.has('endpoint') && this.endpoint) {
471
- this.fetching = true;
472
- this.empty = true;
473
- }
474
- }
475
-
476
- private refreshTickets() {
477
- if (this.ticket) {
478
- let url = `/api/v2/tickets.json?contact=${this.uuid}`;
479
- if (this.ticket) {
480
- url = `${url}&ticket=${this.ticket}`;
481
- }
482
-
483
- getAssets(url).then((tickets: Ticket[]) => {
484
- this.tickets = tickets.reverse();
485
- });
486
- }
487
- }
488
-
489
- public getEventsPane() {
490
- return this.getDiv('.events');
491
- }
492
-
493
- public scrollToBottom(smooth = false) {
494
- const events = this.getEventsPane();
495
- events.scrollTo({
496
- top: events.scrollHeight,
497
- behavior: smooth ? 'smooth' : 'auto'
498
- });
499
- this.showMessageAlert = false;
500
-
501
- window.setTimeout(() => {
502
- events.scrollTo({
503
- top: events.scrollHeight,
504
- behavior: smooth ? 'smooth' : 'auto'
505
- });
506
- }, 0);
507
- }
508
-
509
- public refresh(): void {
510
- this.scheduleRefresh(500);
511
- }
512
-
513
- private getEventGroups(events: ContactEvent[]): EventGroup[] {
514
- const grouped: EventGroup[] = [];
515
- let eventGroup: EventGroup = undefined;
516
- for (const event of events) {
517
- const currentEventGroupType = getEventGroupType(event, this.ticket);
518
- // see if we need a new event group
519
- if (!eventGroup || eventGroup.type !== currentEventGroupType) {
520
- // we have a new type, save our last group
521
- if (eventGroup) {
522
- grouped.push(eventGroup);
523
- }
524
- eventGroup = {
525
- open: false,
526
- events: [event],
527
- type: currentEventGroupType
528
- };
529
- } else {
530
- // our event matches the current group, stuff it in there
531
- eventGroup.events.push(event);
532
- }
533
- }
534
-
535
- if (eventGroup && eventGroup.events.length > 0) {
536
- grouped.push(eventGroup);
537
- }
538
- return grouped;
539
- }
540
-
541
- private scheduleRefresh(wait = -1) {
542
- if (this.endDate) {
543
- return;
544
- }
545
-
546
- let refreshWait = wait;
547
-
548
- if (wait === -1) {
549
- const lastEventTime = this.getLastEventTime();
550
- refreshWait = Math.max(
551
- Math.min((new Date().getTime() - lastEventTime) / 2, MAX_CHAT_REFRESH),
552
- MIN_CHAT_REFRESH
553
- );
554
- }
555
-
556
- // cancel any outstanding timeout
557
- if (wait > -1 && this.refreshTimeout) {
558
- window.clearTimeout(this.refreshTimeout);
559
- }
560
-
561
- this.refreshTimeout = window.setTimeout(() => {
562
- if (this.refreshing) {
563
- this.scheduleRefresh();
564
- this.refreshing = false;
565
- } else {
566
- this.refreshing = true;
567
- }
568
- }, refreshWait);
569
- }
570
-
571
- private reset() {
572
- this.endpoint = null;
573
- this.tickets = null;
574
- this.ticketEvents = {};
575
- this.eventGroups = [];
576
- this.fetching = false;
577
- this.complete = false;
578
- this.nextBefore = null;
579
- this.nextAfter = null;
580
- this.lastHeight = 0;
581
- }
582
-
583
- private handleEventGroupShow(event: MouseEvent) {
584
- const grouping = event.currentTarget as HTMLDivElement;
585
- const groupIndex = parseInt(grouping.getAttribute('data-group-index'));
586
- const eventGroup =
587
- this.eventGroups[this.eventGroups.length - groupIndex - 1];
588
- eventGroup.open = true;
589
- this.requestUpdate('eventGroups');
590
- }
591
-
592
- private handleEventGroupHide(event: MouseEvent) {
593
- event.preventDefault();
594
- event.stopPropagation();
595
-
596
- const grouping = event.currentTarget as HTMLDivElement;
597
- const groupIndex = parseInt(grouping.getAttribute('data-group-index'));
598
- const eventGroup =
599
- this.eventGroups[this.eventGroups.length - groupIndex - 1];
600
-
601
- eventGroup.open = false;
602
-
603
- this.requestUpdate('eventGroups');
604
- }
605
-
606
- private handleScroll() {
607
- const events = this.getEventsPane();
608
- if (events.scrollTop <= SCROLL_THRESHOLD) {
609
- if (this.eventGroups.length > 0 && !this.fetching && !this.complete) {
610
- this.fetching = true;
611
- }
612
- }
613
- }
614
-
615
- private updateMostRecent(newEvent: ContactEvent) {
616
- if (
617
- !this.mostRecentEvent ||
618
- this.mostRecentEvent.type !== newEvent.type ||
619
- this.mostRecentEvent.created_on !== newEvent.created_on
620
- ) {
621
- this.mostRecentEvent = newEvent;
622
- }
623
- }
624
-
625
- private getLastEventTime(): number {
626
- const mostRecentGroup = this.eventGroups[this.eventGroups.length - 1];
627
- if (mostRecentGroup) {
628
- const mostRecentEvent =
629
- mostRecentGroup.events[mostRecentGroup.events.length - 1];
630
- return new Date(mostRecentEvent.created_on).getTime();
631
- }
632
- return 0;
633
- }
634
-
635
- public renderEvent(event: ContactEvent): any {
636
- switch (event.type) {
637
- case Events.IVR_CREATED:
638
- case Events.MESSAGE_CREATED:
639
- case Events.MESSAGE_RECEIVED:
640
- case Events.BROADCAST_CREATED:
641
- if ((event as MsgEvent).created_by) {
642
- (event as MsgEvent).created_by = this.store.getUser(
643
- (event as MsgEvent).created_by.email
644
- );
645
- }
646
-
647
- return renderMsgEvent(event as MsgEvent);
648
-
649
- case Events.FLOW_ENTERED:
650
- case Events.FLOW_EXITED:
651
- return renderFlowEvent(event as FlowEvent);
652
-
653
- case Events.RUN_RESULT_CHANGED:
654
- return renderResultEvent(event as UpdateResultEvent);
655
-
656
- case Events.CONTACT_FIELD_CHANGED:
657
- return renderUpdateEvent(event as UpdateFieldEvent);
658
-
659
- case Events.CONTACT_NAME_CHANGED:
660
- return renderNameChanged(event as NameChangedEvent);
661
-
662
- case Events.CONTACT_URNS_CHANGED:
663
- return renderContactURNsChanged(event as URNsChangedEvent);
664
-
665
- case Events.EMAIL_SENT:
666
- return renderEmailSent(event as EmailSentEvent);
667
-
668
- case Events.INPUT_LABELS_ADDED:
669
- return renderLabelsAdded(event as LabelsAddedEvent);
670
-
671
- case Events.TICKET_OPENED: {
672
- return renderTicketAction(event as TicketEvent, 'opened', !this.ticket);
673
- }
674
- case Events.TICKET_NOTE_ADDED:
675
- return renderNoteCreated(event as TicketEvent);
676
-
677
- case Events.TICKET_ASSIGNED:
678
- return renderTicketAssigned(event as TicketEvent);
679
- case Events.TICKET_REOPENED: {
680
- return renderTicketAction(
681
- event as TicketEvent,
682
- 'reopened',
683
- !this.ticket
684
- );
685
- }
686
- case Events.TICKET_CLOSED:
687
- return renderTicketAction(event as TicketEvent, 'closed', !this.ticket);
688
-
689
- case Events.ERROR:
690
- case Events.FAILURE:
691
- return renderErrorMessage(event as ErrorMessageEvent);
692
- case Events.CONTACT_GROUPS_CHANGED:
693
- return renderContactGroupsEvent(event as ContactGroupsEvent);
694
- case Events.WEBHOOK_CALLED:
695
- return renderWebhookEvent(event as WebhookEvent);
696
- case Events.AIRTIME_TRANSFERRED:
697
- return renderAirtimeTransferredEvent(event as AirtimeTransferredEvent);
698
- case Events.CALL_STARTED:
699
- return renderCallStartedEvent();
700
- case Events.CAMPAIGN_FIRED:
701
- return renderCampaignFiredEvent(event as CampaignFiredEvent);
702
- case Events.CHANNEL_EVENT:
703
- return renderChannelEvent(event as ChannelEvent);
704
- case Events.CONTACT_LANGUAGE_CHANGED:
705
- return renderContactLanguageChangedEvent(
706
- event as ContactLanguageChangedEvent
707
- );
708
- case Events.OPTIN_REQUESTED:
709
- return renderOptinRequested(event as OptinRequestedEvent);
710
- }
711
-
712
- return html`<temba-icon
713
- name="alert-triangle"
714
- style="fill:var(--color-error)"
715
- ></temba-icon>
716
- <div class="description">${event.type}</div>`;
717
- }
718
-
719
- private handleClose(uuid: string) {
720
- this.httpComplete = postJSON(`/api/v2/ticket_actions.json`, {
721
- tickets: [uuid],
722
- action: 'close'
723
- })
724
- .then(() => {
725
- this.refreshTickets();
726
- this.refresh();
727
- this.fireCustomEvent(CustomEventType.ContentChanged, {
728
- ticket: { uuid, status: 'closed' }
729
- });
730
- })
731
- .catch((response: any) => {
732
- console.error(response);
733
- });
734
- }
735
-
736
- public checkForAgentAssignmentEvent(agent: string) {
737
- this.httpComplete = getAssets(
738
- `/api/v2/tickets.json?uuid=${this.ticket}`
739
- ).then((assets: Asset[]) => {
740
- if (assets.length === 1) {
741
- const ticket = assets[0] as Ticket;
742
- if (ticket.assignee && ticket.assignee.email === agent) {
743
- this.fireCustomEvent(CustomEventType.ContentChanged, {
744
- ticket: { uuid: this.ticket, assigned: 'self' }
745
- });
746
- } else {
747
- this.fireCustomEvent(CustomEventType.ContentChanged, {
748
- ticket: {
749
- uuid: this.ticket,
750
- assigned: ticket.assignee ? ticket.assignee : null
751
- }
752
- });
753
- }
754
- }
755
- });
756
- }
757
-
758
- public getEventHandlers() {
759
- return [
760
- {
761
- event: 'scroll',
762
- method: throttle(this.handleScroll, 50)
763
- }
764
- ];
765
- }
766
-
767
- /** Check if a ticket event is no longer represented in a session */
768
- private isPurged(ticket: Ticket): boolean {
769
- return !this.ticketEvents[ticket.uuid];
770
- }
771
-
772
- private handleEventClicked(event) {
773
- const ele = event.target as HTMLDivElement;
774
- if (ele.tagName == 'IMG') {
775
- // if we have one, show in our lightbox
776
- const lightbox = document.querySelector('temba-lightbox') as Lightbox;
777
- if (lightbox) {
778
- lightbox.showElement(ele);
779
- }
780
- }
781
- }
782
-
783
- private renderEventContainer(event: ContactEvent) {
784
- const renderedEvent = html`
785
- <div
786
- @click=${this.handleEventClicked}
787
- class="${this.ticket ? 'active-ticket' : ''} event ${event.type}"
788
- >
789
- ${this.renderEvent(event)}
790
- </div>
791
- ${this.debug ? html`<pre>${JSON.stringify(event, null, 2)}</pre>` : null}
792
- `;
793
- return renderedEvent;
794
- }
795
-
796
- public render(): TemplateResult {
797
- return html`
798
- ${
799
- this.fetching
800
- ? html`<temba-loading units="5" size="10"></temba-loading>`
801
- : html`<div style="height:0em"></div>`
802
- }
803
- <div class="events" @scroll=${this.handleScroll}>
804
- ${this.eventGroups.map((eventGroup: EventGroup, index: number) => {
805
- const grouping = getEventGroupType(eventGroup.events[0], this.ticket);
806
- const groupIndex = this.eventGroups.length - index - 1;
807
-
808
- const classes = getClasses({
809
- grouping: true,
810
- [grouping]: true,
811
- expanded: eventGroup.open
812
- });
813
- return html`<div class="${classes}">
814
- ${grouping === 'verbose'
815
- ? html`<div
816
- class="event-count"
817
- @click=${this.handleEventGroupShow}
818
- data-group-index="${groupIndex}"
819
- >
820
- ${eventGroup.open
821
- ? html`<temba-icon
822
- @click=${this.handleEventGroupHide}
823
- data-group-index="${groupIndex}"
824
- name="x"
825
- clickable
826
- ></temba-icon>`
827
- : html`${eventGroup.events.length}
828
- ${eventGroup.events.length === 1
829
- ? html`event`
830
- : html`events`} `}
831
- </div>`
832
- : null}
833
-
834
- <div class="items">
835
- ${eventGroup.events.map((event: ContactEvent) => {
836
- if (
837
- event.type === Events.TICKET_ASSIGNED &&
838
- (event as TicketEvent).note
839
- ) {
840
- const noteEvent = { ...event };
841
- noteEvent.type = Events.TICKET_NOTE_ADDED;
842
-
843
- return html`${this.renderEventContainer(
844
- noteEvent
845
- )}${this.renderEventContainer(event)}`;
846
- } else {
847
- return this.renderEventContainer(event);
848
- }
849
- })}
850
- </div>
851
- </div>`;
852
- })}
853
- </div>
854
-
855
- ${
856
- this.contact && this.contact.status === 'active'
857
- ? html`<div class="new-messages-container">
858
- <div
859
- @click=${() => {
860
- this.scrollToBottom(true);
861
- }}
862
- class="new-messages ${getClasses({
863
- expanded: this.showMessageAlert
864
- })}"
865
- >
866
- New Messages
867
- </div>
868
- </div>`
869
- : null
870
- }
871
-
872
- </div>
873
- `;
874
- }
875
- }