@nyaruka/temba-components 0.42.1 → 0.43.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 (79) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{dae3a0ce.js → 96498fd6.js} +226 -202
  3. package/dist/index.js +226 -202
  4. package/dist/sw.js +1 -1
  5. package/dist/sw.js.map +1 -1
  6. package/dist/templates/components-body.html +1 -1
  7. package/dist/templates/components-head.html +1 -1
  8. package/out-tsc/src/contacts/ContactChat.js +1 -2
  9. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  10. package/out-tsc/src/contacts/ContactHistory.js +17 -1
  11. package/out-tsc/src/contacts/ContactHistory.js.map +1 -1
  12. package/out-tsc/src/contacts/events.js +64 -58
  13. package/out-tsc/src/contacts/events.js.map +1 -1
  14. package/out-tsc/src/lightbox/Lightbox.js +146 -0
  15. package/out-tsc/src/lightbox/Lightbox.js.map +1 -0
  16. package/out-tsc/src/list/TembaMenu.js +2 -42
  17. package/out-tsc/src/list/TembaMenu.js.map +1 -1
  18. package/out-tsc/src/list/TicketList.js +2 -2
  19. package/out-tsc/src/list/TicketList.js.map +1 -1
  20. package/out-tsc/src/utils/index.js +54 -1
  21. package/out-tsc/src/utils/index.js.map +1 -1
  22. package/out-tsc/src/vectoricon/index.js +1 -1
  23. package/out-tsc/src/vectoricon/index.js.map +1 -1
  24. package/out-tsc/temba-modules.js +2 -0
  25. package/out-tsc/temba-modules.js.map +1 -1
  26. package/out-tsc/test/temba-contact-chat.test.js +16 -2
  27. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  28. package/out-tsc/test/temba-contact-history.test.js +1 -1
  29. package/out-tsc/test/temba-contact-history.test.js.map +1 -1
  30. package/out-tsc/test/temba-lightbox.test.js +28 -0
  31. package/out-tsc/test/temba-lightbox.test.js.map +1 -0
  32. package/out-tsc/test/utils.test.js +1 -2
  33. package/out-tsc/test/utils.test.js.map +1 -1
  34. package/package.json +1 -1
  35. package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
  36. package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
  37. package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
  38. package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
  39. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
  40. package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
  41. package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
  42. package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
  43. package/screenshots/truth/contacts/contact-active-default.png +0 -0
  44. package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
  45. package/screenshots/truth/contacts/contact-active-ticket-closed-show-reopen-button.png +0 -0
  46. package/screenshots/truth/contacts/contact-active-ticket-open-show-chatbox.png +0 -0
  47. package/screenshots/truth/contacts/contact-archived-hide-chatbox.png +0 -0
  48. package/screenshots/truth/contacts/contact-archived-ticket-closed-hide-chatbox.png +0 -0
  49. package/screenshots/truth/contacts/contact-blocked-hide-chatbox.png +0 -0
  50. package/screenshots/truth/contacts/contact-stopped-hide-chatbox.png +0 -0
  51. package/screenshots/truth/contacts/history.png +0 -0
  52. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  53. package/screenshots/truth/lightbox/img.png +0 -0
  54. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  55. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  56. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  57. package/screenshots/truth/menu/menu-root.png +0 -0
  58. package/screenshots/truth/menu/menu-submenu.png +0 -0
  59. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  60. package/screenshots/truth/menu/menu-tasks.png +0 -0
  61. package/src/contacts/ContactChat.ts +1 -2
  62. package/src/contacts/ContactHistory.ts +19 -1
  63. package/src/contacts/events.ts +79 -64
  64. package/src/lightbox/Lightbox.ts +161 -0
  65. package/src/list/TembaMenu.ts +2 -45
  66. package/src/list/TicketList.ts +2 -2
  67. package/src/untyped.d.ts +1 -0
  68. package/src/utils/index.ts +68 -2
  69. package/src/vectoricon/index.ts +1 -1
  70. package/static/css/temba-components.css +4 -1
  71. package/temba-modules.ts +2 -0
  72. package/test/temba-contact-chat.test.ts +16 -3
  73. package/test/temba-contact-history.test.ts +1 -1
  74. package/test/temba-lightbox.test.ts +35 -0
  75. package/test/utils.test.ts +1 -2
  76. package/test-assets/img/meow.jpg +0 -0
  77. package/test-assets/style.css +1 -0
  78. package/web-test-runner.config.mjs +4 -0
  79. package/screenshots/truth/menu/menu-focus.png +0 -0
@@ -1,6 +1,13 @@
1
1
  import { css, html, TemplateResult } from 'lit';
2
2
  import { Msg, ObjectReference, User } from '../interfaces';
3
- import { getClasses, oxford, oxfordFn, oxfordNamed, timeSince } from '../utils';
3
+ import {
4
+ getClasses,
5
+ oxford,
6
+ oxfordFn,
7
+ oxfordNamed,
8
+ renderAvatar,
9
+ timeSince,
10
+ } from '../utils';
4
11
  import { Icon } from '../vectoricon';
5
12
  import { getDisplayName } from './helpers';
6
13
 
@@ -165,11 +172,20 @@ export const getEventStyles = () => {
165
172
  }
166
173
 
167
174
  .msg {
168
- padding: var(--event-padding);
169
- border-radius: 8px;
175
+ border-radius: calc(var(--curvature) * 2.5);
170
176
  border: 2px solid rgba(100, 100, 100, 0.1);
171
177
  max-width: 300px;
172
178
  word-break: break-word;
179
+ overflow: hidden;
180
+ }
181
+
182
+ .msg.attachments-1.no-message {
183
+ border: 2px solid transparent;
184
+ background-color: transparent !important;
185
+ }
186
+
187
+ .msg .text {
188
+ padding: var(--event-padding);
173
189
  }
174
190
 
175
191
  .event.msg_received .msg {
@@ -186,7 +202,13 @@ export const getEventStyles = () => {
186
202
  .event.msg_created .msg,
187
203
  .event.broadcast_created .msg,
188
204
  .event.ivr_created .msg {
189
- background: rgb(231, 243, 255);
205
+ background: var(--color-primary-dark);
206
+ color: white;
207
+ font-weight: 400;
208
+ }
209
+
210
+ .msg.automated {
211
+ background: var(--color-automated) !important;
190
212
  }
191
213
 
192
214
  .webhook_called {
@@ -439,7 +461,17 @@ export const getEventStyles = () => {
439
461
  }
440
462
 
441
463
  .attachments {
442
- margin-top: 1em;
464
+ display: flex;
465
+ flex-wrap: wrap;
466
+ margin: -0.2em;
467
+ }
468
+
469
+ .attachment {
470
+ flex: 1 0 45%;
471
+ border-top: 0.05em solid transparent;
472
+ border-left: 0.05em solid transparent;
473
+ margin-top: 0.05em;
474
+ margin-left: 0.05em;
443
475
  }
444
476
  `;
445
477
  };
@@ -636,36 +668,10 @@ export const getEventGroupType = (event: ContactEvent, ticket: string) => {
636
668
  return 'verbose';
637
669
  };
638
670
 
639
- export const renderAvatar = (user: User, agent = '') => {
640
- const current = user && user.email === agent;
641
- if (user.email === FLOW_USER_ID || !user || !user.first_name) {
642
- return html`<temba-tip text="Automated message" position="top"
643
- ><div class="avatar flow" style="margin-top:0.5em">
644
- <temba-icon size="1" name="${Icon.flow_user}" /></div
645
- ></temba-tip>`;
646
- } else {
647
- return html`<temba-tip
648
- text="${user.first_name + ' ' + user.last_name}"
649
- position="top"
650
- >
651
- <div
652
- class="avatar"
653
- style="
654
- border-radius: 9999px;
655
- display:flex;
656
- align-items:center;
657
- border: 2px solid rgba(0,0,0,.05);
658
- background: ${current ? 'var(--color-primary-dark)' : '#eee'};
659
- color: ${current ? '#fff' : '#999'} ;
660
- font-weight: 400;
661
- padding: 0.5em;
662
- line-height:1.2em;
663
- "
664
- >
665
- ${user.first_name[0] + user.last_name[0]}
666
- </div>
667
- </temba-tip>`;
668
- }
671
+ export const renderUserAvatar = (user: User) => {
672
+ return html`<div style="width:3.5em;font-size:0.8em">
673
+ ${renderAvatar({ user, position: 'left' })}
674
+ </div>`;
669
675
  };
670
676
 
671
677
  export const renderAttachment = (attachment: string): TemplateResult => {
@@ -676,10 +682,12 @@ export const renderAttachment = (attachment: string): TemplateResult => {
676
682
 
677
683
  let inner = null;
678
684
  if (mediaType === 'image') {
679
- inner = html`<div class="linked" onclick="goto(event)" href="${url}"><img src="${url}" style="width:100%;height:auto;display:block"></img></a>`;
685
+ inner = html`
686
+ <img src="${url}" style="height:auto;width:100%;display:block;" />
687
+ `;
680
688
  } else if (ext === 'pdf') {
681
689
  return html`<div
682
- style="width:100%;height:300px;border-radius:var(--curvature);box-shadow:0px 0px 12px 0px rgba(0,0,0,.1), 0px 0px 2px 0px rgba(0,0,0,.15);overflow:hidden"
690
+ style="width:100%;height:300px;border-radius:calc(var(--curvature) * 2.5);box-shadow:0px 0px 12px 0px rgba(0,0,0,.1), 0px 0px 2px 0px rgba(0,0,0,.15);overflow:hidden"
683
691
  ><embed src="${url}#view=Fit" type="application/pdf" frameBorder="0" scrolling="auto" height="100%" width="100%"></embed></div>`;
684
692
  } else if (mediaType === 'video') {
685
693
  return html`<video
@@ -724,11 +732,7 @@ export const renderAttachment = (attachment: string): TemplateResult => {
724
732
  </div>`;
725
733
  }
726
734
 
727
- return html`<div
728
- style="width:100%;max-width:300px;border-radius:var(--curvature); box-shadow:0px 0px 6px 0px rgba(0,0,0,.15);overflow:hidden"
729
- >
730
- ${inner}
731
- </div>`;
735
+ return html`<div style="">${inner}</div>`;
732
736
  };
733
737
 
734
738
  export const renderMsgEvent = (
@@ -741,6 +745,7 @@ export const renderMsgEvent = (
741
745
 
742
746
  // summary items which appear under the message bubble
743
747
  const summary: TemplateResult[] = [];
748
+
744
749
  if (event.logs_url) {
745
750
  summary.push(html` <div class="icon-link">
746
751
  <temba-icon
@@ -786,17 +791,32 @@ export const renderMsgEvent = (
786
791
 
787
792
  return html`<div style="display:flex;align-items:flex-start">
788
793
  <div style="display:flex;flex-direction:column">
789
- ${event.msg.text ? html`<div class="msg">${event.msg.text}</div>` : null}
790
- ${event.msg.attachments
791
- ? html`<div class="attachments">
792
- ${event.msg.attachments.map(attachment =>
793
- renderAttachment(attachment)
794
- )}
795
- </div> `
796
- : null}
794
+ <div
795
+ class="${event.msg.text ? '' : 'no-message'} attachments-${(
796
+ event.msg.attachments || []
797
+ ).length} ${getClasses({
798
+ msg: true,
799
+ automated: !isInbound && !event.msg.created_by,
800
+ })}"
801
+ >
802
+ ${event.msg.text
803
+ ? html` <div class="text">${event.msg.text}</div> `
804
+ : null}
805
+ ${event.msg.attachments
806
+ ? html`<div class="attachments">
807
+ ${event.msg.attachments.map(
808
+ attachment =>
809
+ html` <div class="attachment">
810
+ ${renderAttachment(attachment)}
811
+ </div>`
812
+ )}
813
+ </div> `
814
+ : null}
815
+ </div>
797
816
  ${!event.msg.text && !event.msg.attachments
798
817
  ? html`<div class="unsupported">Unsupported Message</div>`
799
818
  : null}
819
+
800
820
  <div
801
821
  class="msg-summary"
802
822
  style="flex-direction:row${isInbound ? '-reverse' : ''}"
@@ -807,12 +827,10 @@ export const renderMsgEvent = (
807
827
  </div>
808
828
 
809
829
  ${!isInbound
810
- ? html`<div style="margin-left:0.8em;margin-top:0.3em">
830
+ ? html`<div style="margin-left:0.8em;margin-top:0.3em;font-size:0.9em">
811
831
  ${event.msg.created_by
812
- ? html`<div style="font-size:0.8em">
813
- ${renderAvatar(event.msg.created_by, agent)}
814
- </div>`
815
- : renderAvatar({ email: FLOW_USER_ID })}
832
+ ? renderUserAvatar(event.msg.created_by)
833
+ : renderUserAvatar(null)}
816
834
  </div>`
817
835
  : null}
818
836
  </div>`;
@@ -934,10 +952,7 @@ export const renderLabelsAdded = (event: LabelsAddedEvent): TemplateResult => {
934
952
  `;
935
953
  };
936
954
 
937
- export const renderNoteCreated = (
938
- event: TicketEvent,
939
- agent: string
940
- ): TemplateResult => {
955
+ export const renderNoteCreated = (event: TicketEvent): TemplateResult => {
941
956
  return html`<div style="display:flex;align-items:flex-start">
942
957
  <div style="display:flex;flex-direction:column">
943
958
  <div class="description">${event.note}</div>
@@ -951,7 +966,7 @@ export const renderNoteCreated = (
951
966
  </div>
952
967
  </div>
953
968
  <div style="margin-left:0.8em;margin-top:0.3em;font-size:0.8em">
954
- ${renderAvatar(event.created_by, agent)}
969
+ ${renderUserAvatar(event.created_by)}
955
970
  </div>
956
971
  </div>`;
957
972
  };
@@ -1187,16 +1202,16 @@ export const renderCampaignFiredEvent = (
1187
1202
  Campaign
1188
1203
  <span
1189
1204
  class="linked"
1190
- onclick="goto(event)"
1191
- href="/campaign/read/${event.campaign.uuid}"
1205
+ onclick="goto(event, this)"
1206
+ href="/campaign/read/${event.campaign.uuid}/"
1192
1207
  >${event.campaign.name}</span
1193
1208
  >
1194
1209
  ${event.fired_result === 'S' ? 'skipped' : 'triggered'}
1195
1210
  <span
1196
1211
  class="linked"
1197
- onclick="goto(event)"
1212
+ onclick="goto(event, this)"
1198
1213
  href="/campaignevent/read/${event.campaign.uuid}/${event.campaign_event
1199
- .id}"
1214
+ .id}/"
1200
1215
  >
1201
1216
  ${event.campaign_event.offset_display}
1202
1217
  ${event.campaign_event.relative_to.name}</span
@@ -0,0 +1,161 @@
1
+ import { css, html, PropertyValueMap } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { RapidElement } from '../RapidElement';
4
+ import { getClasses } from '../utils';
5
+ import { styleMap } from 'lit-html/directives/style-map.js';
6
+
7
+ /**
8
+ * This component relies on a bit of sleight of hand magic
9
+ * to achieve it's effect. As such, it requires the use of
10
+ * computed animation times and window.setTimeout().
11
+ */
12
+ export class Lightbox extends RapidElement {
13
+ static get styles() {
14
+ return css`
15
+ :host {
16
+ position: absolute;
17
+ }
18
+
19
+ .mask {
20
+ display: flex;
21
+ opacity: 0;
22
+ background: rgba(0, 0, 0, 0.5);
23
+ position: absolute;
24
+ height: 100vh;
25
+ width: 100vw;
26
+ pointer-events: none;
27
+ }
28
+
29
+ .zoom .mask {
30
+ opacity: 1;
31
+ pointer-events: auto;
32
+ }
33
+
34
+ .matte {
35
+ position: absolute;
36
+ transform: translate(400, 400) scale(3, 3);
37
+ border-radius: 2%;
38
+ overflow: hidden;
39
+ box-shadow: 0 0 12px 3px rgba(0, 0, 0, 0.15);
40
+ }
41
+ `;
42
+ }
43
+
44
+ @property({ type: Number })
45
+ animationTime = 300;
46
+
47
+ @property({ type: Boolean })
48
+ show = false;
49
+
50
+ @property({ type: Boolean })
51
+ zoom = false;
52
+
53
+ @property({ type: Number })
54
+ zoomPct = 0.9;
55
+
56
+ private ele: HTMLElement;
57
+ private left: number;
58
+ private top: number;
59
+ private height: number;
60
+ private width: number;
61
+ private scale = 1;
62
+ private xTrans = '0px';
63
+ private yTrans = '0px';
64
+
65
+ protected updated(
66
+ changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
67
+ ): void {
68
+ if (changed.has('show') && this.show) {
69
+ window.setTimeout(() => {
70
+ this.zoom = true;
71
+ }, 0);
72
+ }
73
+
74
+ if (changed.has('zoom') && !this.zoom && this.show) {
75
+ window.setTimeout(() => {
76
+ this.show = false;
77
+ }, this.animationTime);
78
+ }
79
+ }
80
+
81
+ public showElement(ele: HTMLElement) {
82
+ // size our matte according to the ele's boundaries
83
+ const bounds = ele.getBoundingClientRect();
84
+ this.ele = ele.cloneNode() as HTMLElement;
85
+ this.left = bounds.left;
86
+ this.top = bounds.top;
87
+ this.width = bounds.width;
88
+ this.height = bounds.height;
89
+
90
+ this.xTrans = '0px';
91
+ this.yTrans = '0px';
92
+ this.scale = 1;
93
+
94
+ let desiredWidth = this.width;
95
+ let desiredHeight = this.height;
96
+
97
+ // set our destination and size
98
+ if (this.height > this.width) {
99
+ desiredHeight = window.innerHeight * this.zoomPct;
100
+ this.scale = desiredHeight / this.height;
101
+ desiredWidth = this.width * this.scale;
102
+ }
103
+ // landscape
104
+ else {
105
+ desiredWidth = window.innerWidth * this.zoomPct;
106
+ this.scale = desiredWidth / this.width;
107
+ desiredHeight = this.height * this.scale;
108
+ }
109
+
110
+ const xGrowth = (desiredWidth - this.width) / 2;
111
+ const xDest = (window.innerWidth - desiredWidth) / 2;
112
+ this.xTrans = xDest - this.left + xGrowth + 'px';
113
+
114
+ const yGrowth = (desiredHeight - this.height) / 2;
115
+ const yDest = (window.innerHeight - desiredHeight) / 2;
116
+ this.yTrans = yDest - this.top + yGrowth + 'px';
117
+ this.show = true;
118
+ }
119
+
120
+ public handleClick() {
121
+ this.zoom = false;
122
+ }
123
+
124
+ public render() {
125
+ const styles = {
126
+ transition: `transform ${this.animationTime}ms ease, box-shadow ${this.animationTime}ms ease`,
127
+ };
128
+
129
+ if (this.show) {
130
+ styles['left'] = this.left + 'px';
131
+ styles['top'] = this.top + 'px';
132
+ styles['width'] = this.width + 'px';
133
+ styles['height'] = this.height + 'px';
134
+ }
135
+
136
+ if (this.zoom) {
137
+ styles[
138
+ 'transform'
139
+ ] = `translate(${this.xTrans}, ${this.yTrans}) scale(${this.scale}, ${this.scale})`;
140
+ }
141
+
142
+ return html`
143
+ <div
144
+ class=${getClasses({
145
+ container: true,
146
+ show: this.show,
147
+ zoom: this.zoom,
148
+ })}
149
+ @click=${this.handleClick}
150
+ >
151
+ <div
152
+ class=${getClasses({ mask: true })}
153
+ style="transition: all ${this.animationTime}ms; ease"
154
+ ></div>
155
+ <div class=${getClasses({ matte: true })} style=${styleMap(styles)}>
156
+ ${this.show ? this.ele : null}
157
+ </div>
158
+ </div>
159
+ `;
160
+ }
161
+ }
@@ -2,9 +2,8 @@ import { css, html, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { CustomEventType } from '../interfaces';
4
4
  import { RapidElement } from '../RapidElement';
5
- import { debounce, fetchResults, getClasses } from '../utils';
5
+ import { debounce, fetchResults, getClasses, renderAvatar } from '../utils';
6
6
  import { Icon } from '../vectoricon';
7
- import ColorHash from 'color-hash';
8
7
 
9
8
  export interface MenuItem {
10
9
  id?: string;
@@ -166,19 +165,6 @@ export class TembaMenu extends RapidElement {
166
165
  align-items: center;
167
166
  }
168
167
 
169
- .avatar-circle {
170
- box-shadow: 0 0 0px 3px rgba(0, 0, 0, 0.075);
171
- display: flex;
172
- margin: 0.4em 0em;
173
- height: 2em;
174
- width: 2em;
175
- flex-direction: row;
176
- align-items: center;
177
- color: #fff;
178
- border-radius: 100%;
179
- font-weight: 400;
180
- }
181
-
182
168
  temba-dropdown > div[slot='dropdown'] .avatar .avatar-circle {
183
169
  font-size: 0.7em;
184
170
  }
@@ -836,35 +822,6 @@ export class TembaMenu extends RapidElement {
836
822
  return expanded;
837
823
  }
838
824
 
839
- private renderAvatar(avatar: string) {
840
- const hash = new ColorHash();
841
- const color = hash.hex(avatar);
842
-
843
- let second = avatar.indexOf(' ') + 1;
844
- if (second < 1) {
845
- second = avatar.length > 1 ? 1 : 0;
846
- }
847
- let name = avatar.substring(0, 1) + avatar.substring(second, second + 1);
848
- name = name.toUpperCase();
849
-
850
- return html`
851
- <div
852
- style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;"
853
- >
854
- <div
855
- class="avatar-circle"
856
- style="border: 0px solid rgba(0,0,0,.1);background:${color}"
857
- >
858
- <div
859
- style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;flex-grow:1"
860
- >
861
- <div style="border:0px solid blue;">${name}</div>
862
- </div>
863
- </div>
864
- </div>
865
- `;
866
- }
867
-
868
825
  private renderMenuItem = (
869
826
  menuItem: MenuItem,
870
827
  parent: MenuItem = null
@@ -929,7 +886,7 @@ export class TembaMenu extends RapidElement {
929
886
  });
930
887
 
931
888
  if (menuItem.avatar) {
932
- icon = this.renderAvatar(menuItem.avatar);
889
+ icon = renderAvatar({ name: menuItem.avatar });
933
890
  }
934
891
 
935
892
  const item = html` <div
@@ -3,7 +3,7 @@ import { property } from 'lit/decorators.js';
3
3
  import { TembaList } from './TembaList';
4
4
  import { timeSince } from '../utils';
5
5
  import { Contact } from '../interfaces';
6
- import { renderAvatar } from '../contacts/events';
6
+ import { renderUserAvatar } from '../contacts/events';
7
7
  import { Icon } from '../vectoricon';
8
8
 
9
9
  export class TicketList extends TembaList {
@@ -84,7 +84,7 @@ export class TicketList extends TembaList {
84
84
  </div>
85
85
  <div style="font-size:0.7em;">
86
86
  ${!contact.ticket.closed_on && contact.ticket.assignee
87
- ? html`${renderAvatar(contact.ticket.assignee, this.agent)}`
87
+ ? html`${renderUserAvatar(contact.ticket.assignee)}`
88
88
  : null}
89
89
  </div>
90
90
  </div>
package/src/untyped.d.ts CHANGED
@@ -16,3 +16,4 @@ declare function moveMouse(x: number, y: number);
16
16
  declare function mouseDown();
17
17
  declare function mouseUp();
18
18
  declare function setViewport({}: any);
19
+ declare function waitForNetworkIdle();
@@ -2,7 +2,10 @@
2
2
  import { html, TemplateResult } from 'lit-html';
3
3
  import { Button } from '../button/Button';
4
4
  import { Dialog } from '../dialog/Dialog';
5
- import { ContactField, Ticket } from '../interfaces';
5
+ import { ContactField, Ticket, User } from '../interfaces';
6
+ import ColorHash from 'color-hash';
7
+
8
+ const colorHash = new ColorHash();
6
9
 
7
10
  export type Asset = KeyedAsset & Ticket & ContactField;
8
11
 
@@ -108,7 +111,7 @@ export const getClasses = (map: ClassMap): string => {
108
111
  if (result.trim().length > 0) {
109
112
  result = ' ' + result;
110
113
  }
111
- return result;
114
+ return result.trim();
112
115
  };
113
116
 
114
117
  export const fetchResultsPage = (
@@ -613,3 +616,66 @@ export const formatFileSize = (bytes: number, decimalPoint: number): string => {
613
616
  i = Math.floor(Math.log(bytes) / Math.log(k));
614
617
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
615
618
  };
619
+
620
+ export const renderAvatar = (input: {
621
+ name?: string;
622
+ user?: User;
623
+ icon?: string;
624
+ image?: string;
625
+ position?: string;
626
+ }) => {
627
+ if (!input.position) {
628
+ input.position = 'right';
629
+ }
630
+
631
+ // just a url
632
+ if (input.image) {
633
+ return html`<img src="${input.image}" />`;
634
+ }
635
+
636
+ let text = input.name;
637
+ if (input.user) {
638
+ text = `${input.user.first_name} ${input.user.last_name}`;
639
+ }
640
+
641
+ if (!text) {
642
+ return null;
643
+ }
644
+
645
+ const color = colorHash.hex(text);
646
+ let second = text.indexOf(' ') + 1;
647
+ if (second < 1) {
648
+ second = text.length > 1 ? 1 : 0;
649
+ }
650
+ let initials = text.substring(0, 1) + text.substring(second, second + 1);
651
+ initials = initials.toUpperCase();
652
+
653
+ return html`
654
+ <temba-tip text=${text} position=${input.position}>
655
+ <div
656
+ style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;"
657
+ >
658
+ <div
659
+ class="avatar-circle"
660
+ style="
661
+ display: flex;
662
+ height: 2em;
663
+ width: 2em;
664
+ flex-direction: row;
665
+ align-items: center;
666
+ color: #fff;
667
+ border-radius: 100%;
668
+ font-weight: 400;
669
+ border: 0.3em solid rgba(0,0,0,.05);
670
+ background:${color}"
671
+ >
672
+ <div
673
+ style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;flex-grow:1"
674
+ >
675
+ <div style="border:0px solid blue;">${initials}</div>
676
+ </div>
677
+ </div>
678
+ </div>
679
+ </temba-tip>
680
+ `;
681
+ };
@@ -40,7 +40,7 @@ export enum Icon {
40
40
  flow_interrupted = 'x-close',
41
41
  flow_ivr = 'phone-call-01',
42
42
  flow_message = 'message-square-02',
43
- flow_user = 'activity',
43
+ flow_user = 'hard-drive',
44
44
  flows = 'flow',
45
45
  group = 'users-01',
46
46
  group_smart = 'atom-01',
@@ -105,6 +105,8 @@
105
105
  --color-error: rgb(var(--error-rgb));
106
106
  --color-alert: rgb(var(--error-rgb));
107
107
 
108
+ --color-automated: rgb(78,205,106);
109
+
108
110
  --icon-color: var(--text-color);
109
111
  --icon-color-hover: var(--icon-color);
110
112
  --icon-color-circle-hover: rgba(245, 245, 245, .8);
@@ -139,7 +141,6 @@
139
141
  --button-y: 6px;
140
142
  --button-x: 14px;
141
143
 
142
- --menu-padding: 1em;
143
144
  --bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
144
145
 
145
146
  --temba-charcount-counts-margin-top: 4px;
@@ -148,6 +149,8 @@
148
149
  --temba-charcount-summary-right: 0px;
149
150
  --temba-charcount-summary-bottom: 0px;
150
151
 
152
+ --color-automated: rgb(78,205,106);
153
+
151
154
  }
152
155
 
153
156
  temba-button {
package/temba-modules.ts CHANGED
@@ -45,6 +45,7 @@ import { ContentMenu } from './src/list/ContentMenu';
45
45
  import { TembaDate } from './src/date/TembaDate';
46
46
  import Remote from './src/remote/Remote';
47
47
  import { Compose } from './src/compose/Compose';
48
+ import { Lightbox } from './src/lightbox/Lightbox';
48
49
 
49
50
  export function addCustomElement(name: string, comp: any) {
50
51
  if (!window.customElements.get(name)) {
@@ -63,6 +64,7 @@ addCustomElement('temba-checkbox', Checkbox);
63
64
  addCustomElement('temba-select', Select);
64
65
  addCustomElement('temba-options', Options);
65
66
  addCustomElement('temba-loading', Loading);
67
+ addCustomElement('temba-lightbox', Lightbox);
66
68
  addCustomElement('temba-button', Button);
67
69
  addCustomElement('temba-omnibox', Omnibox);
68
70
  addCustomElement('temba-tip', Tip);
@@ -536,13 +536,20 @@ describe('temba-contact-chat - ticket tests', () => {
536
536
  chat.refresh();
537
537
  await chat.httpComplete;
538
538
 
539
+ let lastScroll = 0;
539
540
  await assertScreenshot(
540
541
  'contacts/contact-active-ticket-closed-show-reopen-button',
541
542
  getClip(chat),
542
543
  {
543
544
  clock: clock,
544
545
  predicate: () => {
545
- return chat.getContactHistory().getEventsPane().scrollTop === 921;
546
+ const currentScroll = chat
547
+ .getContactHistory()
548
+ .getEventsPane().scrollTop;
549
+ if (currentScroll !== 0 && currentScroll === lastScroll) {
550
+ return true;
551
+ }
552
+ lastScroll = currentScroll;
546
553
  },
547
554
  }
548
555
  );
@@ -562,14 +569,20 @@ describe('temba-contact-chat - ticket tests', () => {
562
569
  chat.currentTicket = tickets.items[0];
563
570
  chat.refresh();
564
571
  await chat.httpComplete;
565
-
572
+ let lastScroll = 0;
566
573
  await assertScreenshot(
567
574
  'contacts/contact-archived-ticket-closed-hide-chatbox',
568
575
  getClip(chat),
569
576
  {
570
577
  clock: clock,
571
578
  predicate: () => {
572
- return chat.getContactHistory().getEventsPane().scrollTop === 870;
579
+ const currentScroll = chat
580
+ .getContactHistory()
581
+ .getEventsPane().scrollTop;
582
+ if (currentScroll !== 0 && currentScroll === lastScroll) {
583
+ return true;
584
+ }
585
+ lastScroll = currentScroll;
573
586
  },
574
587
  }
575
588
  );