@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.
- package/CHANGELOG.md +15 -0
- package/demo/index.html +5 -3
- package/dist/{a386634d.js → 2102a010.js} +862 -120
- package/dist/index.js +862 -120
- package/dist/static/svg/index.svg +1 -1
- package/dist/sw.js +1 -1
- package/dist/sw.js.map +1 -1
- package/dist/templates/components-body.html +1 -1
- package/dist/templates/components-head.html +1 -1
- package/out-tsc/src/contacts/ContactHistory.js +20 -13
- package/out-tsc/src/contacts/ContactHistory.js.map +1 -1
- package/out-tsc/src/contacts/ContactTickets.js +27 -25
- package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
- package/out-tsc/src/contacts/events.js +7 -13
- package/out-tsc/src/contacts/events.js.map +1 -1
- package/out-tsc/src/imagepicker/CroppieCSS.js +254 -0
- package/out-tsc/src/imagepicker/CroppieCSS.js.map +1 -0
- package/out-tsc/src/imagepicker/ImagePicker.js +246 -0
- package/out-tsc/src/imagepicker/ImagePicker.js.map +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/TembaMenu.js +1 -1
- package/out-tsc/src/list/TembaMenu.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +15 -12
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/mask/Mask.js +36 -0
- package/out-tsc/src/mask/Mask.js.map +1 -0
- package/out-tsc/src/store/Store.js +3 -0
- package/out-tsc/src/store/Store.js.map +1 -1
- package/out-tsc/src/store/StoreElement.js +11 -30
- package/out-tsc/src/store/StoreElement.js.map +1 -1
- package/out-tsc/src/store/StoreMonitorElement.js +50 -0
- package/out-tsc/src/store/StoreMonitorElement.js.map +1 -0
- package/out-tsc/src/user/TembaUser.js +107 -0
- package/out-tsc/src/user/TembaUser.js.map +1 -0
- package/out-tsc/src/utils/index.js +25 -17
- package/out-tsc/src/utils/index.js.map +1 -1
- package/out-tsc/src/vectoricon/index.js +4 -1
- package/out-tsc/src/vectoricon/index.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +515 -0
- package/out-tsc/src/webchat/WebChat.js.map +1 -0
- package/out-tsc/src/webchat/index.js +7 -0
- package/out-tsc/src/webchat/index.js.map +1 -0
- package/out-tsc/temba-modules.js +8 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-contact-tickets.test.js +1 -1
- package/out-tsc/test/temba-contact-tickets.test.js.map +1 -1
- package/package.json +4 -2
- package/screenshots/truth/contacts/tickets-assignment.png +0 -0
- package/screenshots/truth/contacts/tickets.png +0 -0
- package/screenshots/truth/menu/menu-focused-with items.png +0 -0
- package/screenshots/truth/menu/menu-refresh-1.png +0 -0
- package/screenshots/truth/menu/menu-refresh-2.png +0 -0
- package/screenshots/truth/menu/menu-root.png +0 -0
- package/screenshots/truth/menu/menu-submenu.png +0 -0
- package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
- package/screenshots/truth/menu/menu-tasks.png +0 -0
- package/src/contacts/ContactHistory.ts +28 -14
- package/src/contacts/ContactTickets.ts +28 -36
- package/src/contacts/events.ts +8 -25
- package/src/imagepicker/CroppieCSS.ts +254 -0
- package/src/imagepicker/ImagePicker.ts +269 -0
- package/src/interfaces.ts +2 -1
- package/src/list/TembaMenu.ts +1 -1
- package/src/list/TicketList.ts +15 -12
- package/src/mask/Mask.ts +32 -0
- package/src/store/Store.ts +4 -0
- package/src/store/StoreElement.ts +13 -38
- package/src/store/StoreMonitorElement.ts +61 -0
- package/src/untyped.d.ts +1 -0
- package/src/user/TembaUser.ts +111 -0
- package/src/utils/index.ts +26 -19
- package/src/vectoricon/index.ts +4 -1
- package/src/webchat/WebChat.ts +559 -0
- package/src/webchat/index.ts +6 -0
- package/static/svg/index.svg +1 -1
- package/static/svg/webchat.svg +1 -0
- package/static/svg/work/traced/camera-01.svg +1 -0
- package/static/svg/work/traced/send-03.svg +1 -0
- package/static/svg/work/used/camera-01.svg +4 -0
- package/static/svg/work/used/send-03.svg +3 -0
- package/svg.js +12 -8
- package/temba-modules.ts +8 -0
- package/test/temba-contact-tickets.test.ts +1 -3
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
744
|
-
|
|
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="
|
|
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:
|
|
754
|
-
width:
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
`;
|
package/src/vectoricon/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// for cache busting we dynamically generate a fingerprint, use yarn svg to update
|
|
2
|
-
export const SVG_FINGERPRINT = '
|
|
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
|
+
}
|