@nyaruka/temba-components 0.67.4 → 0.70.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 +15 -0
  2. package/demo/index.html +5 -3
  3. package/dist/{a386634d.js → 2102a010.js} +862 -120
  4. package/dist/index.js +862 -120
  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/ContactHistory.js +20 -13
  11. package/out-tsc/src/contacts/ContactHistory.js.map +1 -1
  12. package/out-tsc/src/contacts/ContactTickets.js +27 -25
  13. package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
  14. package/out-tsc/src/contacts/events.js +7 -13
  15. package/out-tsc/src/contacts/events.js.map +1 -1
  16. package/out-tsc/src/imagepicker/CroppieCSS.js +254 -0
  17. package/out-tsc/src/imagepicker/CroppieCSS.js.map +1 -0
  18. package/out-tsc/src/imagepicker/ImagePicker.js +246 -0
  19. package/out-tsc/src/imagepicker/ImagePicker.js.map +1 -0
  20. package/out-tsc/src/interfaces.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 +15 -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/store/Store.js +3 -0
  28. package/out-tsc/src/store/Store.js.map +1 -1
  29. package/out-tsc/src/store/StoreElement.js +11 -30
  30. package/out-tsc/src/store/StoreElement.js.map +1 -1
  31. package/out-tsc/src/store/StoreMonitorElement.js +50 -0
  32. package/out-tsc/src/store/StoreMonitorElement.js.map +1 -0
  33. package/out-tsc/src/user/TembaUser.js +107 -0
  34. package/out-tsc/src/user/TembaUser.js.map +1 -0
  35. package/out-tsc/src/utils/index.js +25 -17
  36. package/out-tsc/src/utils/index.js.map +1 -1
  37. package/out-tsc/src/vectoricon/index.js +4 -1
  38. package/out-tsc/src/vectoricon/index.js.map +1 -1
  39. package/out-tsc/src/webchat/WebChat.js +515 -0
  40. package/out-tsc/src/webchat/WebChat.js.map +1 -0
  41. package/out-tsc/src/webchat/index.js +7 -0
  42. package/out-tsc/src/webchat/index.js.map +1 -0
  43. package/out-tsc/temba-modules.js +8 -0
  44. package/out-tsc/temba-modules.js.map +1 -1
  45. package/out-tsc/test/temba-contact-tickets.test.js +1 -1
  46. package/out-tsc/test/temba-contact-tickets.test.js.map +1 -1
  47. package/package.json +4 -2
  48. package/screenshots/truth/contacts/tickets-assignment.png +0 -0
  49. package/screenshots/truth/contacts/tickets.png +0 -0
  50. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  51. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  52. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  53. package/screenshots/truth/menu/menu-root.png +0 -0
  54. package/screenshots/truth/menu/menu-submenu.png +0 -0
  55. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  56. package/screenshots/truth/menu/menu-tasks.png +0 -0
  57. package/src/contacts/ContactHistory.ts +28 -14
  58. package/src/contacts/ContactTickets.ts +28 -36
  59. package/src/contacts/events.ts +8 -25
  60. package/src/imagepicker/CroppieCSS.ts +254 -0
  61. package/src/imagepicker/ImagePicker.ts +269 -0
  62. package/src/interfaces.ts +2 -1
  63. package/src/list/TembaMenu.ts +1 -1
  64. package/src/list/TicketList.ts +15 -12
  65. package/src/mask/Mask.ts +32 -0
  66. package/src/store/Store.ts +4 -0
  67. package/src/store/StoreElement.ts +13 -38
  68. package/src/store/StoreMonitorElement.ts +61 -0
  69. package/src/untyped.d.ts +1 -0
  70. package/src/user/TembaUser.ts +111 -0
  71. package/src/utils/index.ts +26 -19
  72. package/src/vectoricon/index.ts +4 -1
  73. package/src/webchat/WebChat.ts +559 -0
  74. package/src/webchat/index.ts +6 -0
  75. package/static/svg/index.svg +1 -1
  76. package/static/svg/webchat.svg +1 -0
  77. package/static/svg/work/traced/camera-01.svg +1 -0
  78. package/static/svg/work/traced/send-03.svg +1 -0
  79. package/static/svg/work/used/camera-01.svg +4 -0
  80. package/static/svg/work/used/send-03.svg +3 -0
  81. package/svg.js +12 -8
  82. package/temba-modules.ts +8 -0
  83. package/test/temba-contact-tickets.test.ts +1 -3
@@ -7,7 +7,7 @@ import ColorHash from 'color-hash';
7
7
 
8
8
  export const DEFAULT_MEDIA_ENDPOINT = '/api/v2/media.json';
9
9
 
10
- const colorHash = new ColorHash();
10
+ export const colorHash = new ColorHash();
11
11
 
12
12
  export type Asset = KeyedAsset & Ticket & ContactField;
13
13
 
@@ -718,21 +718,16 @@ export const renderAvatar = (input: {
718
718
  name?: string;
719
719
  user?: User;
720
720
  icon?: string;
721
- image?: string;
722
721
  position?: string;
723
722
  tip?: boolean;
723
+ scale?: number;
724
724
  }) => {
725
725
  if (!input.position) {
726
726
  input.position = 'right';
727
727
  }
728
728
 
729
- // just a url
730
- if (input.image) {
731
- return html`<img src="${input.image}" />`;
732
- }
733
-
734
729
  let text = input.name;
735
- if (input.user) {
730
+ if (input.user && input.user.first_name && input.user.last_name) {
736
731
  text = `${input.user.first_name} ${input.user.last_name}`;
737
732
  }
738
733
 
@@ -740,31 +735,43 @@ export const renderAvatar = (input: {
740
735
  return null;
741
736
  }
742
737
 
743
- const color = colorHash.hex(text);
744
- const initials = extractInitials(text);
738
+ let initials = '';
739
+ let color = colorHash.hex(text);
740
+ // just a url
741
+ if (input.user && input.user.avatar) {
742
+ color = `url('${input.user.avatar}') center / contain no-repeat`;
743
+ } else if (text) {
744
+ initials = extractInitials(text);
745
+ }
746
+
745
747
  const avatar = html`
746
748
  <div
747
- style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;"
749
+ style="display:flex; flex-direction: column; align-items:center;transform:scale(${input.scale ||
750
+ 1});"
748
751
  >
749
752
  <div
750
753
  class="avatar-circle"
751
754
  style="
752
755
  display: flex;
753
- height: 2em;
754
- width: 2em;
756
+ height: 30px;
757
+ width: 30px;
755
758
  flex-direction: row;
756
759
  align-items: center;
757
760
  color: #fff;
758
761
  border-radius: 100%;
759
762
  font-weight: 400;
760
- border: 0.3em solid rgba(0,0,0,.05);
763
+ overflow: hidden;
764
+ font-size: 12px;
765
+ box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.1);
761
766
  background:${color}"
762
767
  >
763
- <div
764
- style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;flex-grow:1"
765
- >
766
- <div style="border:0px solid blue;">${initials}</div>
767
- </div>
768
+ ${initials
769
+ ? html` <div
770
+ style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;flex-grow:1"
771
+ >
772
+ <div style="border:0px solid blue;">${initials}</div>
773
+ </div>`
774
+ : null}
768
775
  </div>
769
776
  </div>
770
777
  `;
@@ -1,5 +1,5 @@
1
1
  // for cache busting we dynamically generate a fingerprint, use yarn svg to update
2
- export const SVG_FINGERPRINT = '963a11882da2ee8b18c878e911d8eaf1';
2
+ export const SVG_FINGERPRINT = '819ccb7b7ad3480361d5afba355b4e6f';
3
3
 
4
4
  // only icons below are included in the sprite sheet
5
5
  export enum Icon {
@@ -162,6 +162,7 @@ export enum Icon {
162
162
  search = 'search-refraction',
163
163
  select_open = 'chevron-down',
164
164
  select_clear = 'x',
165
+ send = 'send-03',
165
166
  service = 'magic-wand-01',
166
167
  service_end = 'log-out-04',
167
168
  settings = 'settings-02',
@@ -171,6 +172,7 @@ export enum Icon {
171
172
  sort_down = 'sort-arrow-down',
172
173
  sort_up = 'sort-arrow-up',
173
174
  staff = 'hard-drive',
175
+ submit = 'check',
174
176
  tickets = 'agent',
175
177
  tickets_all = 'archive',
176
178
  tickets_closed = 'check',
@@ -199,6 +201,7 @@ export enum Icon {
199
201
  updated = 'edit-02',
200
202
  up = 'chevron-up',
201
203
  upload = 'upload-cloud-01',
204
+ upload_image = 'camera-01',
202
205
  usages = 'link-04',
203
206
  user = 'users-01',
204
207
  users = 'users-01',
@@ -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
+ }