@nyaruka/temba-components 0.67.3 → 0.68.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 (68) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/demo/index.html +5 -3
  3. package/dist/{314b925e.js → dd629ee4.js} +802 -122
  4. package/dist/index.js +802 -122
  5. package/dist/static/svg/index.svg +1 -1
  6. package/dist/sw.js +1 -1
  7. package/dist/sw.js.map +1 -1
  8. package/dist/templates/components-body.html +1 -1
  9. package/dist/templates/components-head.html +1 -1
  10. package/out-tsc/src/contacts/ContactTickets.js +4 -6
  11. package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
  12. package/out-tsc/src/contacts/events.js +3 -8
  13. package/out-tsc/src/contacts/events.js.map +1 -1
  14. package/out-tsc/src/imagepicker/CroppieCSS.js +254 -0
  15. package/out-tsc/src/imagepicker/CroppieCSS.js.map +1 -0
  16. package/out-tsc/src/imagepicker/ImagePicker.js +248 -0
  17. package/out-tsc/src/imagepicker/ImagePicker.js.map +1 -0
  18. package/out-tsc/src/interfaces.js.map +1 -1
  19. package/out-tsc/src/list/NotificationList.js +4 -0
  20. package/out-tsc/src/list/NotificationList.js.map +1 -1
  21. package/out-tsc/src/list/TembaMenu.js +1 -1
  22. package/out-tsc/src/list/TembaMenu.js.map +1 -1
  23. package/out-tsc/src/list/TicketList.js +16 -12
  24. package/out-tsc/src/list/TicketList.js.map +1 -1
  25. package/out-tsc/src/mask/Mask.js +36 -0
  26. package/out-tsc/src/mask/Mask.js.map +1 -0
  27. package/out-tsc/src/utils/index.js +24 -16
  28. package/out-tsc/src/utils/index.js.map +1 -1
  29. package/out-tsc/src/vectoricon/index.js +5 -1
  30. package/out-tsc/src/vectoricon/index.js.map +1 -1
  31. package/out-tsc/src/webchat/WebChat.js +515 -0
  32. package/out-tsc/src/webchat/WebChat.js.map +1 -0
  33. package/out-tsc/src/webchat/index.js +7 -0
  34. package/out-tsc/src/webchat/index.js.map +1 -0
  35. package/out-tsc/temba-modules.js +6 -0
  36. package/out-tsc/temba-modules.js.map +1 -1
  37. package/package.json +4 -2
  38. package/screenshots/truth/contacts/tickets-assignment.png +0 -0
  39. package/screenshots/truth/contacts/tickets.png +0 -0
  40. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  41. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  42. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  43. package/screenshots/truth/menu/menu-root.png +0 -0
  44. package/screenshots/truth/menu/menu-submenu.png +0 -0
  45. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  46. package/screenshots/truth/menu/menu-tasks.png +0 -0
  47. package/src/contacts/ContactTickets.ts +4 -6
  48. package/src/contacts/events.ts +3 -9
  49. package/src/imagepicker/CroppieCSS.ts +254 -0
  50. package/src/imagepicker/ImagePicker.ts +272 -0
  51. package/src/interfaces.ts +2 -1
  52. package/src/list/NotificationList.ts +3 -0
  53. package/src/list/TembaMenu.ts +1 -1
  54. package/src/list/TicketList.ts +16 -12
  55. package/src/mask/Mask.ts +32 -0
  56. package/src/untyped.d.ts +1 -0
  57. package/src/utils/index.ts +26 -18
  58. package/src/vectoricon/index.ts +5 -1
  59. package/src/webchat/WebChat.ts +559 -0
  60. package/src/webchat/index.ts +6 -0
  61. package/static/svg/index.svg +1 -1
  62. package/static/svg/webchat.svg +1 -0
  63. package/static/svg/work/traced/camera-01.svg +1 -0
  64. package/static/svg/work/traced/send-03.svg +1 -0
  65. package/static/svg/work/used/camera-01.svg +4 -0
  66. package/static/svg/work/used/send-03.svg +3 -0
  67. package/svg.js +12 -8
  68. package/temba-modules.ts +6 -0
@@ -0,0 +1,559 @@
1
+ /* eslint-disable @typescript-eslint/no-this-alias */
2
+ import { LitElement, TemplateResult, html, css, PropertyValueMap } from 'lit';
3
+ import { property } from 'lit/decorators.js';
4
+
5
+ interface Message {
6
+ text: string;
7
+ type: string;
8
+ identifier?: string;
9
+ origin?: string;
10
+ timestamp: number;
11
+ }
12
+
13
+ // how long of a window to show time between batches
14
+ const BATCH_TIME_WINDOW = 30 * 60 * 1000;
15
+
16
+ const TIME_FORMAT = { hour: 'numeric', minute: '2-digit' } as any;
17
+ const DAY_FORMAT = {
18
+ weekday: undefined,
19
+ year: 'numeric',
20
+ month: 'short',
21
+ day: 'numeric',
22
+ } as any;
23
+ const VERBOSE_FORMAT = {
24
+ weekday: undefined,
25
+ year: 'numeric',
26
+ month: 'short',
27
+ day: 'numeric',
28
+ hour: 'numeric',
29
+ minute: '2-digit',
30
+ } as any;
31
+
32
+ export class WebChat extends LitElement {
33
+ static get styles() {
34
+ return css`
35
+ :host {
36
+ display: flex-inline;
37
+ align-items: center;
38
+ align-self: center;
39
+ --curvature: 0.6em;
40
+ --color-primary: hsla(208, 70%, 55%, 1);
41
+ }
42
+
43
+ .block {
44
+ margin-bottom: 1em;
45
+ }
46
+
47
+ .time {
48
+ text-align: center;
49
+ font-size: 0.8em;
50
+ color: #999;
51
+ margin-top: 2em;
52
+ border-top: 1px solid #f8f8f8;
53
+ padding: 1em;
54
+ margin-left: 4em;
55
+ margin-right: 4em;
56
+ }
57
+
58
+ .first .time {
59
+ margin-top: 0;
60
+ border-top: none;
61
+ padding-top: 0;
62
+ }
63
+
64
+ .row {
65
+ display: flex;
66
+ flex-direction: row;
67
+ align-items: flex-start;
68
+ }
69
+
70
+ .input-panel {
71
+ padding: 1em;
72
+ background: #fff;
73
+ }
74
+
75
+ .avatar {
76
+ margin-top: 0.6em;
77
+ margin-right: 0.6em;
78
+ flex-shrink: 0;
79
+ width: 2em;
80
+ height: 2em;
81
+ overflow: hidden;
82
+ border-radius: 100%;
83
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 3px 7px 0px,
84
+ rgba(0, 0, 0, 0.2) 0px 1px 2px 0px,
85
+ inset 0 0 0 0.15em rgba(0, 0, 0, 0.1);
86
+ }
87
+
88
+ .toggle {
89
+ flex-shrink: 0;
90
+ width: 4em;
91
+ height: 4em;
92
+ overflow: hidden;
93
+ border-radius: 100%;
94
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1em 0.7em,
95
+ rgba(0, 0, 0, 0.2) 0px 1px 2px 0px,
96
+ inset 0 0 0 0.25em rgba(0, 0, 0, 0.1);
97
+ cursor: pointer;
98
+ transition: box-shadow 0.2s ease-out;
99
+ position: absolute;
100
+ bottom: 1em;
101
+ right: 1em;
102
+ }
103
+
104
+ .toggle:hover {
105
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1em 0.7em,
106
+ rgba(0, 0, 0, 0.4) 0px 1px 2px 0px,
107
+ inset 0 0 0 0.25em rgba(0, 0, 0, 0.2);
108
+ }
109
+
110
+ .incoming .row {
111
+ flex-direction: row-reverse;
112
+ margin-left: 1em;
113
+ }
114
+
115
+ .bubble {
116
+ padding: 1em;
117
+ padding-bottom: 0.5em;
118
+ background: #fafafa;
119
+ border-radius: var(--curvature);
120
+ }
121
+
122
+ .bubble .name {
123
+ font-size: 0.9em;
124
+ font-weight: 400;
125
+ margin-bottom: 0.5em;
126
+ }
127
+
128
+ .outgoing .bubble {
129
+ border-top-left-radius: 0;
130
+ }
131
+
132
+ .incoming .bubble {
133
+ background: var(--color-primary);
134
+ color: white;
135
+ border-top-right-radius: 0;
136
+ }
137
+
138
+ .message {
139
+ margin-bottom: 0.5em;
140
+ }
141
+
142
+ .chat {
143
+ max-width: 50vw;
144
+ width: 28rem;
145
+ border-radius: var(--curvature);
146
+ overflow: hidden;
147
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 3px 7px 0px,
148
+ rgba(0, 0, 0, 0.2) 0px 1px 2px 0px, rgba(0, 0, 0, 0.1) 5em 5em 5em 5em;
149
+ position: absolute;
150
+ bottom: 2em;
151
+ right: 1em;
152
+ transition: all 0.2s ease-out;
153
+ transform: scale(0.9);
154
+ pointer-events: none;
155
+ opacity: 0;
156
+ }
157
+
158
+ .chat.open {
159
+ bottom: 6em;
160
+ opacity: 1;
161
+ transform: scale(1);
162
+ pointer-events: initial;
163
+ }
164
+
165
+ .messages {
166
+ background: #fff;
167
+ }
168
+
169
+ .scroll {
170
+ height: 40rem;
171
+ max-height: 60vh;
172
+ overflow: auto;
173
+ -webkit-overflow-scrolling: touch;
174
+ overflow-scrolling: touch;
175
+ padding: 1em 1em 0 1em;
176
+ }
177
+
178
+ .messages:before {
179
+ content: '';
180
+ background: /* Shadow TOP */ radial-gradient(
181
+ farthest-side at 50% 0,
182
+ rgba(0, 0, 0, 0.2),
183
+ rgba(0, 0, 0, 0)
184
+ )
185
+ center top;
186
+ height: 10px;
187
+ display: block;
188
+ position: absolute;
189
+ max-width: 50vw;
190
+ width: 28rem;
191
+ transition: opacity 0.1s ease-out;
192
+ }
193
+
194
+ .messages:after {
195
+ content: '';
196
+ background: /* Shadow BOTTOM */ radial-gradient(
197
+ farthest-side at 50% 100%,
198
+ rgba(0, 0, 0, 0.2),
199
+ rgba(0, 0, 0, 0)
200
+ )
201
+ center bottom;
202
+ height: 10px;
203
+ display: block;
204
+ position: absolute;
205
+ margin-top: -10px;
206
+ max-width: 50vw;
207
+ width: 28rem;
208
+ margin-right: 5em;
209
+ transition: opacity 0.1s ease-out;
210
+ }
211
+
212
+ .scroll-at-top .messages:before {
213
+ opacity: 0;
214
+ }
215
+
216
+ .scroll-at-bottom .messages:after {
217
+ opacity: 0;
218
+ }
219
+
220
+ .input {
221
+ border: none;
222
+ flex-grow: 1;
223
+ }
224
+
225
+ .input:focus {
226
+ outline: none;
227
+ }
228
+
229
+ input::placeholder {
230
+ opacity: 0.3;
231
+ }
232
+
233
+ .input.inactive {
234
+ //pointer-events: none;
235
+ //opacity: 0.3;
236
+ }
237
+
238
+ .active {
239
+ }
240
+
241
+ .send-icon {
242
+ color: #eee;
243
+ pointer-events: none;
244
+ transform: rotate(-45deg);
245
+ transition: transform 0.2s ease-out;
246
+ }
247
+
248
+ .pending .send-icon {
249
+ color: var(--color-primary);
250
+ pointer-events: initial;
251
+ transform: rotate(0deg);
252
+ }
253
+ `;
254
+ }
255
+
256
+ @property({ type: String })
257
+ channel: string;
258
+
259
+ @property({ type: String })
260
+ urn: string;
261
+
262
+ @property({ type: Array })
263
+ messages: Message[][] = [];
264
+
265
+ // is our socket connection established
266
+ @property({ type: Boolean })
267
+ active: boolean;
268
+
269
+ // is the chat widget open
270
+ @property({ type: Boolean })
271
+ open = false;
272
+
273
+ @property({ type: Boolean })
274
+ hasPendingText = false;
275
+
276
+ @property({ type: Boolean, attribute: false })
277
+ hideTopScroll = true;
278
+
279
+ @property({ type: Boolean, attribute: false })
280
+ hideBottomScroll = true;
281
+
282
+ private sock: WebSocket;
283
+
284
+ public constructor() {
285
+ super();
286
+ }
287
+
288
+ private openSocket(): void {
289
+ console.log('opening socket..');
290
+ const webChat = this;
291
+ let url = `ws://localhost:8070/start?channel=${this.channel}`;
292
+ if (this.urn) {
293
+ url = `${url}&identifier=${this.urn}`;
294
+ }
295
+ this.sock = new WebSocket(url);
296
+ this.sock.onclose = function (event) {
297
+ console.log('socket closed');
298
+ webChat.active = false;
299
+ };
300
+ this.sock.onmessage = function (event) {
301
+ console.log(event.data);
302
+ const msg = JSON.parse(event.data) as Message;
303
+ if (msg.type === 'chat_started') {
304
+ if (webChat.urn !== msg.identifier) {
305
+ webChat.messages = [];
306
+ }
307
+ webChat.urn = msg.identifier;
308
+ webChat.active = true;
309
+ webChat.requestUpdate('messages');
310
+ } else if (msg.type === 'chat_resumed') {
311
+ webChat.urn = msg.identifier;
312
+ webChat.active = true;
313
+ } else if (msg.type === 'msg_out') {
314
+ msg['timestamp'] = new Date().getTime();
315
+ webChat.addMessage(msg);
316
+ webChat.requestUpdate('messages');
317
+ }
318
+ };
319
+ }
320
+
321
+ private restoreFromLocal(): void {
322
+ console.log('Restoring from localStorage..');
323
+ const data = JSON.parse(localStorage.getItem('temba-chat') || '{}');
324
+ const urn = 'urn' in data ? data['urn'] : null;
325
+ if (urn && !this.urn) {
326
+ this.urn = urn;
327
+ const messages = 'messages' in data ? data['messages'] : [];
328
+ this.messages.push(...messages);
329
+ }
330
+ }
331
+
332
+ private writeToLocal(): void {
333
+ console.log('Writing to localStorage..');
334
+ if (this.urn) {
335
+ const data = { urn: this.urn, messages: this.messages, version: 1 };
336
+ localStorage.setItem('temba-chat', JSON.stringify(data));
337
+ }
338
+ }
339
+
340
+ public firstUpdated(
341
+ changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
342
+ ): void {
343
+ super.firstUpdated(changed);
344
+ }
345
+
346
+ private focusInput() {
347
+ const input = this.shadowRoot.querySelector('.input') as any;
348
+ input.focus();
349
+ }
350
+
351
+ public updated(
352
+ changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
353
+ ): void {
354
+ super.updated(changed);
355
+
356
+ if (this.open && changed.has('open') && changed.get('open') !== undefined) {
357
+ const scroll = this.shadowRoot.querySelector('.scroll');
358
+ const hasScroll = scroll.scrollHeight > scroll.clientHeight;
359
+ this.hideBottomScroll = true;
360
+ this.hideTopScroll = !hasScroll;
361
+ this.scrollToBottom();
362
+ this.focusInput();
363
+
364
+ if (!this.active) {
365
+ this.openSocket();
366
+ }
367
+ }
368
+
369
+ if (changed.has('channel')) {
370
+ this.restoreFromLocal();
371
+ }
372
+
373
+ if (changed.has('messages')) {
374
+ console.log('messages changed', this.messages);
375
+ this.writeToLocal();
376
+ console.log(this.messages);
377
+ this.scrollToBottom();
378
+ }
379
+ }
380
+
381
+ private addMessage(msg: Message) {
382
+ let lastGroup =
383
+ this.messages.length > 0 ? this.messages[this.messages.length - 1] : [];
384
+ const isSame = lastGroup.length === 0 || lastGroup[0].origin === msg.origin;
385
+ if (!isSame) {
386
+ lastGroup = [];
387
+ }
388
+ if (lastGroup.length === 0) {
389
+ this.messages.push(lastGroup);
390
+ }
391
+ lastGroup.push(msg);
392
+ }
393
+
394
+ public openChat(): void {
395
+ this.open = true;
396
+ }
397
+
398
+ public handleKeyUp(event: any) {
399
+ if (this.hasPendingText && event.key === 'Enter') {
400
+ this.sendPendingMessage();
401
+ }
402
+
403
+ this.hasPendingText = event.target.value.length > 0;
404
+ }
405
+
406
+ private sendPendingMessage() {
407
+ if (this.active) {
408
+ const input = this.shadowRoot.querySelector('.input') as any;
409
+ const text = input.value;
410
+ input.value = '';
411
+
412
+ const msg = {
413
+ type: 'msg_in',
414
+ text: text,
415
+ timestamp: new Date().getTime(),
416
+ };
417
+
418
+ this.addMessage(msg);
419
+ this.sock.send(JSON.stringify(msg));
420
+ this.requestUpdate('messages');
421
+ this.hasPendingText = input.value.length > 0;
422
+ }
423
+ }
424
+
425
+ private scrollToBottom() {
426
+ const scroll = this.shadowRoot.querySelector('.scroll');
427
+ if (scroll) {
428
+ scroll.scrollTop = scroll.scrollHeight;
429
+ this.hideBottomScroll = true;
430
+ }
431
+ }
432
+
433
+ private renderMessageGroup(
434
+ messages: Message[],
435
+ idx: number,
436
+ groups: Message[][]
437
+ ): TemplateResult {
438
+ let lastBatchTime = null;
439
+ if (idx > 0) {
440
+ const lastGroup = groups[idx - 1];
441
+ if (lastGroup && lastGroup.length > 0) {
442
+ lastBatchTime = lastGroup[lastGroup.length - 1].timestamp;
443
+ }
444
+ }
445
+
446
+ const newBatchTime = messages[0].timestamp;
447
+ const showTime = newBatchTime - lastBatchTime > BATCH_TIME_WINDOW;
448
+
449
+ let timeDisplay = null;
450
+ if (showTime) {
451
+ let lastTime = null;
452
+ const newTime = new Date(newBatchTime);
453
+ if (lastBatchTime) {
454
+ lastTime = new Date(lastBatchTime);
455
+ }
456
+ const showDay = !lastTime || newTime.getDate() !== lastTime.getDate();
457
+ if (showDay) {
458
+ timeDisplay = html`<div class="time">
459
+ ${newTime.toLocaleDateString(undefined, DAY_FORMAT)}
460
+ </div>`;
461
+ } else {
462
+ timeDisplay = html`<div class="time">
463
+ ${newTime.toLocaleTimeString(undefined, TIME_FORMAT)}
464
+ </div>`;
465
+ }
466
+ }
467
+
468
+ const blockTime = new Date(messages[messages.length - 1].timestamp);
469
+ const incoming = !messages[0].origin;
470
+
471
+ return html` <div
472
+ class="block ${incoming ? 'incoming' : 'outgoing'} ${idx === 0
473
+ ? 'first'
474
+ : ''}"
475
+ title="${blockTime.toLocaleTimeString(undefined, VERBOSE_FORMAT)}"
476
+ >
477
+ ${timeDisplay}
478
+ <div class="row">
479
+ ${!incoming
480
+ ? html`
481
+ <div
482
+ class="avatar"
483
+ style="background: center / contain no-repeat url(https://dl-textit.s3.amazonaws.com/orgs/6418/media/5e81/5e814c83-bf33-43ea-b6c1-ee46f8acaf34/avatar.jpg)"
484
+ ></div>
485
+ `
486
+ : null}
487
+
488
+ <div class="bubble">
489
+ ${!incoming ? html`<div class="name">Henry McHelper</div>` : null}
490
+ ${messages.map(msg => html`<div class="message">${msg.text}</div>`)}
491
+ </div>
492
+ </div>
493
+ </div>`;
494
+ }
495
+
496
+ private handleScroll(event: any) {
497
+ this.hideBottomScroll =
498
+ Math.round(event.target.scrollTop + event.target.clientHeight) >=
499
+ event.target.scrollHeight;
500
+ this.hideTopScroll = event.target.scrollTop === 0;
501
+ }
502
+
503
+ private handleClickInputPanel(event: any) {
504
+ const input = this.shadowRoot.querySelector('.input') as any;
505
+ input.focus();
506
+ }
507
+
508
+ private toggleChat() {
509
+ this.open = !this.open;
510
+ }
511
+
512
+ public render(): TemplateResult {
513
+ return html`
514
+ <div
515
+ class="chat ${this.hideTopScroll ? 'scroll-at-top' : ''} ${this
516
+ .hideBottomScroll
517
+ ? 'scroll-at-bottom'
518
+ : ''} ${this.open ? 'open' : ''}"
519
+ >
520
+ <div class="messages">
521
+ <div class="scroll" @scroll=${this.handleScroll}>
522
+ ${this.messages
523
+ ? this.messages.map(
524
+ (msgGroup, idx, groups) =>
525
+ html`${this.renderMessageGroup(msgGroup, idx, groups)}`
526
+ )
527
+ : null}
528
+ </div>
529
+ </div>
530
+ <div
531
+ class="row input-panel ${this.hasPendingText ? 'pending' : ''}"
532
+ @click=${this.handleClickInputPanel}
533
+ >
534
+ <input
535
+ class="input ${this.active ? 'active' : 'inactive'}"
536
+ type="text"
537
+ placeholder="Message.."
538
+ @keydown=${this.handleKeyUp}
539
+ />
540
+ <temba-icon
541
+ tabindex="1"
542
+ class="send-icon"
543
+ name="send"
544
+ size="1"
545
+ clickable
546
+ @click=${this.sendPendingMessage}
547
+ ></temba-icon>
548
+ </div>
549
+ </div>
550
+
551
+ <div @click=${this.toggleChat}>
552
+ <div
553
+ class="toggle"
554
+ style="background: center / contain no-repeat url(https://dl-textit.s3.amazonaws.com/orgs/6418/media/5e81/5e814c83-bf33-43ea-b6c1-ee46f8acaf34/avatar.jpg)"
555
+ ></div>
556
+ </div>
557
+ `;
558
+ }
559
+ }
@@ -0,0 +1,6 @@
1
+ export const SVG_FINGERPRINT = 'febafb41c2fd60efa2bdaead993c7087';
2
+
3
+ // webchat spritesheet
4
+ export enum WebChatIcon {
5
+ send = 'send-03',
6
+ }