@nyaruka/temba-components 0.91.6 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/demo/index.html +1 -1
- package/dist/temba-components.js +760 -1189
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chat/Chat.js +714 -0
- package/out-tsc/src/chat/Chat.js.map +1 -0
- package/out-tsc/src/completion/helpers.js +1 -29
- package/out-tsc/src/completion/helpers.js.map +1 -1
- package/out-tsc/src/compose/Compose.js +6 -2
- package/out-tsc/src/compose/Compose.js.map +1 -1
- package/out-tsc/src/contacts/ContactChat.js +518 -54
- package/out-tsc/src/contacts/ContactChat.js.map +1 -1
- package/out-tsc/src/contacts/events.js +1 -998
- package/out-tsc/src/contacts/events.js.map +1 -1
- package/out-tsc/src/lightbox/Lightbox.js +4 -0
- package/out-tsc/src/lightbox/Lightbox.js.map +1 -1
- package/out-tsc/src/list/TembaMenu.js +0 -1
- package/out-tsc/src/list/TembaMenu.js.map +1 -1
- package/out-tsc/src/markdown.js +33 -0
- package/out-tsc/src/markdown.js.map +1 -0
- package/out-tsc/src/select/Select.js +6 -1
- package/out-tsc/src/select/Select.js.map +1 -1
- package/out-tsc/src/textinput/TextInput.js +1 -1
- package/out-tsc/src/textinput/TextInput.js.map +1 -1
- package/out-tsc/src/thumbnail/Thumbnail.js +128 -81
- package/out-tsc/src/thumbnail/Thumbnail.js.map +1 -1
- package/out-tsc/src/utils/index.js +9 -11
- package/out-tsc/src/utils/index.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +109 -358
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/src/webchat/index.js +17 -0
- package/out-tsc/src/webchat/index.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -2
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/temba-webchat.js +2 -0
- package/out-tsc/temba-webchat.js.map +1 -1
- package/out-tsc/test/temba-contact-chat.test.js +1 -0
- package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
- package/out-tsc/test/temba-lightbox.test.js +4 -4
- package/out-tsc/test/temba-lightbox.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
- package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
- package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
- package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
- package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
- package/screenshots/truth/contacts/contact-active-default.png +0 -0
- package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
- package/screenshots/truth/contacts/contact-archived-hide-chatbox.png +0 -0
- package/screenshots/truth/contacts/contact-blocked-hide-chatbox.png +0 -0
- package/screenshots/truth/contacts/contact-stopped-hide-chatbox.png +0 -0
- package/screenshots/truth/lightbox/img-zoomed.png +0 -0
- package/screenshots/truth/lightbox/img.png +0 -0
- package/src/chat/Chat.ts +791 -0
- package/src/completion/helpers.ts +2 -40
- package/src/compose/Compose.ts +6 -2
- package/src/contacts/ContactChat.ts +609 -59
- package/src/contacts/events.ts +1 -1068
- package/src/lightbox/Lightbox.ts +5 -0
- package/src/list/TembaMenu.ts +0 -1
- package/src/markdown.ts +41 -0
- package/src/select/Select.ts +5 -1
- package/src/textinput/TextInput.ts +1 -1
- package/src/thumbnail/Thumbnail.ts +130 -81
- package/src/utils/index.ts +12 -13
- package/src/webchat/WebChat.ts +196 -413
- package/src/webchat/index.ts +23 -1
- package/static/css/temba-components.css +2 -0
- package/temba-modules.ts +2 -2
- package/temba-webchat.ts +2 -0
- package/test/temba-contact-chat.test.ts +1 -0
- package/test/temba-lightbox.test.ts +4 -4
- package/test-assets/contacts/history.json +1 -56
- package/out-tsc/src/contacts/ContactHistory.js +0 -691
- package/out-tsc/src/contacts/ContactHistory.js.map +0 -1
- package/out-tsc/test/temba-contact-history.test.js +0 -69
- package/out-tsc/test/temba-contact-history.test.js.map +0 -1
- package/src/contacts/ContactHistory.ts +0 -875
- package/test/temba-contact-history.test.ts +0 -107
|
@@ -1,12 +1,243 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-this-alias */
|
|
2
|
+
import { css, html, PropertyValueMap, TemplateResult } from 'lit';
|
|
2
3
|
import { property } from 'lit/decorators.js';
|
|
3
4
|
import { Contact, CustomEventType, Ticket } from '../interfaces';
|
|
4
|
-
import { postJSON } from '../utils';
|
|
5
|
-
import { ContactHistory } from './ContactHistory';
|
|
5
|
+
import { oxford, oxfordFn, postJSON } from '../utils';
|
|
6
6
|
import { ContactStoreElement } from './ContactStoreElement';
|
|
7
7
|
import { Compose } from '../compose/Compose';
|
|
8
|
+
import { fetchContactHistory, getDisplayName } from './helpers';
|
|
9
|
+
import {
|
|
10
|
+
AirtimeTransferredEvent,
|
|
11
|
+
CampaignFiredEvent,
|
|
12
|
+
ChannelEvent,
|
|
13
|
+
ContactEvent,
|
|
14
|
+
ContactGroupsEvent,
|
|
15
|
+
ContactHistoryPage,
|
|
16
|
+
ContactLanguageChangedEvent,
|
|
17
|
+
EmailSentEvent,
|
|
18
|
+
ErrorMessageEvent,
|
|
19
|
+
FlowEvent,
|
|
20
|
+
LabelsAddedEvent,
|
|
21
|
+
MsgEvent,
|
|
22
|
+
NameChangedEvent,
|
|
23
|
+
OptinRequestedEvent,
|
|
24
|
+
TicketEvent,
|
|
25
|
+
UpdateFieldEvent,
|
|
26
|
+
UpdateResultEvent,
|
|
27
|
+
URNsChangedEvent,
|
|
28
|
+
WebhookEvent
|
|
29
|
+
} from './events';
|
|
30
|
+
import { Chat, ChatEvent, MessageType } from '../chat/Chat';
|
|
31
|
+
import { getUserDisplay } from '../webchat';
|
|
32
|
+
import { DEFAULT_AVATAR } from '../webchat/assets';
|
|
33
|
+
|
|
34
|
+
export enum Events {
|
|
35
|
+
MESSAGE_CREATED = 'msg_created',
|
|
36
|
+
MESSAGE_RECEIVED = 'msg_received',
|
|
37
|
+
BROADCAST_CREATED = 'broadcast_created',
|
|
38
|
+
IVR_CREATED = 'ivr_created',
|
|
39
|
+
FLOW_ENTERED = 'flow_entered',
|
|
40
|
+
|
|
41
|
+
FLOW_EXITED = 'flow_exited',
|
|
42
|
+
RUN_RESULT_CHANGED = 'run_result_changed',
|
|
43
|
+
CONTACT_FIELD_CHANGED = 'contact_field_changed',
|
|
44
|
+
CONTACT_GROUPS_CHANGED = 'contact_groups_changed',
|
|
45
|
+
CONTACT_NAME_CHANGED = 'contact_name_changed',
|
|
46
|
+
CONTACT_URNS_CHANGED = 'contact_urns_changed',
|
|
47
|
+
CAMPAIGN_FIRED = 'campaign_fired',
|
|
48
|
+
CHANNEL_EVENT = 'channel_event',
|
|
49
|
+
CONTACT_LANGUAGE_CHANGED = 'contact_language_changed',
|
|
50
|
+
WEBHOOK_CALLED = 'webhook_called',
|
|
51
|
+
AIRTIME_TRANSFERRED = 'airtime_transferred',
|
|
52
|
+
CALL_STARTED = 'call_started',
|
|
53
|
+
EMAIL_SENT = 'email_sent',
|
|
54
|
+
INPUT_LABELS_ADDED = 'input_labels_added',
|
|
55
|
+
NOTE_CREATED = 'note_created',
|
|
56
|
+
TICKET_ASSIGNED = 'ticket_assigned',
|
|
57
|
+
TICKET_NOTE_ADDED = 'ticket_note_added',
|
|
58
|
+
TICKET_CLOSED = 'ticket_closed',
|
|
59
|
+
TICKET_OPENED = 'ticket_opened',
|
|
60
|
+
TICKET_REOPENED = 'ticket_reopened',
|
|
61
|
+
OPTIN_REQUESTED = 'optin_requested',
|
|
62
|
+
ERROR = 'error',
|
|
63
|
+
FAILURE = 'failure'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const renderInfoList = (singular: string, plural: string, items: any[]) => {
|
|
67
|
+
if (items.length === 1) {
|
|
68
|
+
return `${singular} **${items[0].name}**`;
|
|
69
|
+
} else {
|
|
70
|
+
const list = items.map((item) => `**${item.name}**`);
|
|
71
|
+
if (list.length === 2) {
|
|
72
|
+
return `${plural} ${list.join(' and ')}`;
|
|
73
|
+
} else {
|
|
74
|
+
const last = list.pop();
|
|
75
|
+
return `${plural} ${list.join(', ')}, and ${last}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const toTitleCase = (str: string) => {
|
|
81
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const renderChannelEvent = (event: ChannelEvent): string => {
|
|
85
|
+
if (event.event.type === 'mt_miss') {
|
|
86
|
+
return 'Missed outgoing call';
|
|
87
|
+
} else if (event.event.type === 'mo_miss') {
|
|
88
|
+
return 'Missed incoming call';
|
|
89
|
+
} else if (event.event.type === 'new_conversation') {
|
|
90
|
+
return 'Started conversation';
|
|
91
|
+
} else if (event.channel_event_type === 'welcome_message') {
|
|
92
|
+
return 'Welcome Message Sent';
|
|
93
|
+
} else if (event.event.type === 'referral') {
|
|
94
|
+
return 'Referred';
|
|
95
|
+
} else if (event.event.type === 'follow') {
|
|
96
|
+
return 'Followed';
|
|
97
|
+
} else if (event.event.type === 'stop_contact') {
|
|
98
|
+
return 'Stopped';
|
|
99
|
+
} else if (event.event.type === 'mt_call') {
|
|
100
|
+
return 'Outgoing Phone Call';
|
|
101
|
+
} else if (event.event.type == 'mo_call') {
|
|
102
|
+
return 'Incoming Phone call';
|
|
103
|
+
} else if (event.event.type == 'optin') {
|
|
104
|
+
return `Opted in to **${event.event.optin?.name}**`;
|
|
105
|
+
} else if (event.event.type == 'optout') {
|
|
106
|
+
return `Opted out of **${event.event.optin?.name}**`;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const renderFlowEvent = (event: FlowEvent): string => {
|
|
111
|
+
let verb = 'Interrupted';
|
|
112
|
+
if (event.status !== 'I') {
|
|
113
|
+
if (event.type === Events.FLOW_ENTERED) {
|
|
114
|
+
verb = 'Started';
|
|
115
|
+
} else {
|
|
116
|
+
verb = 'Completed';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return `${verb} [**${event.flow.name}**](/flow/editor/${event.flow.uuid}/)`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const renderResultEvent = (event: UpdateResultEvent): string => {
|
|
123
|
+
if (!event.name.startsWith('_') && event.value) {
|
|
124
|
+
return `Updated flow result **${event.name}** to **${event.value}**`;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const renderUpdateEvent = (event: UpdateFieldEvent): string => {
|
|
129
|
+
return event.value
|
|
130
|
+
? `Updated **${event.field.name}** to **${event.value.text}**`
|
|
131
|
+
: `Cleared **${event.field.name}**`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const renderNameChanged = (event: NameChangedEvent): string => {
|
|
135
|
+
return `Updated **Contact Name** to **${event.name}**`;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const renderContactURNsChanged = (event: URNsChangedEvent): string => {
|
|
139
|
+
return `Updated **URNs** to ${oxfordFn(
|
|
140
|
+
event.urns,
|
|
141
|
+
(urn: string) => `**${urn.split(':')[1].split('?')[0]}**`
|
|
142
|
+
)}`;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const renderEmailSent = (event: EmailSentEvent): string => {
|
|
146
|
+
return `Email sent to **${oxford(event.to, 'and')}** with subject **${
|
|
147
|
+
event.subject
|
|
148
|
+
}**`;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const renderLabelsAdded = (event: LabelsAddedEvent): string => {
|
|
152
|
+
return `Applied ${renderInfoList('label', 'labels', event.labels)}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const renderTicketAction = (
|
|
156
|
+
event: TicketEvent,
|
|
157
|
+
action: string
|
|
158
|
+
): string => {
|
|
159
|
+
if (event.created_by) {
|
|
160
|
+
return `**${getUserDisplay(
|
|
161
|
+
event.created_by
|
|
162
|
+
)}** ${action} a **[ticket](/ticket/all/closed/${event.ticket.uuid}/)**`;
|
|
163
|
+
}
|
|
164
|
+
return `A **[ticket](/ticket/all/closed/${event.ticket.uuid}/)** was **${action}**`;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const renderTicketAssigned = (event: TicketEvent): string => {
|
|
168
|
+
return event.assignee
|
|
169
|
+
? event.assignee.id === event.created_by.id
|
|
170
|
+
? `**${getDisplayName(event.created_by)}** took this ticket`
|
|
171
|
+
: `${getDisplayName(
|
|
172
|
+
event.created_by
|
|
173
|
+
)} assigned this ticket to **${getDisplayName(event.assignee)}**`
|
|
174
|
+
: `**${getDisplayName(event.created_by)}** unassigned this ticket`;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const renderContactGroupsEvent = (event: ContactGroupsEvent): string => {
|
|
178
|
+
const groupsEvent = event as ContactGroupsEvent;
|
|
179
|
+
if (groupsEvent.groups_added) {
|
|
180
|
+
return renderInfoList(
|
|
181
|
+
'Added to group',
|
|
182
|
+
'Added to groups',
|
|
183
|
+
groupsEvent.groups_added
|
|
184
|
+
);
|
|
185
|
+
} else if (groupsEvent.groups_removed) {
|
|
186
|
+
return renderInfoList(
|
|
187
|
+
'Removed from group',
|
|
188
|
+
'Removed from groups',
|
|
189
|
+
groupsEvent.groups_removed
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const renderCampaignFiredEvent = (event: CampaignFiredEvent): string => {
|
|
195
|
+
return `Campaign ${event.campaign.name}
|
|
196
|
+
${event.fired_result === 'S' ? 'skipped' : 'triggered'}
|
|
197
|
+
${event.campaign_event.offset_display}
|
|
198
|
+
${event.campaign_event.relative_to.name}`;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const renderTicketOpened = (event: TicketEvent): string => {
|
|
202
|
+
return `${event.ticket.topic.name} ticket was opened`;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const renderErrorMessage = (event: ErrorMessageEvent): string => {
|
|
206
|
+
return `${event.text} ${
|
|
207
|
+
event.type === Events.FAILURE
|
|
208
|
+
? `Run ended prematurely, check the flow design`
|
|
209
|
+
: null
|
|
210
|
+
}`;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const renderWebhookEvent = (event: WebhookEvent): string => {
|
|
214
|
+
return event.status === 'success'
|
|
215
|
+
? `Successfully called ${event.url}`
|
|
216
|
+
: `Failed to call ${event.url}`;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const renderAirtimeTransferredEvent = (
|
|
220
|
+
event: AirtimeTransferredEvent
|
|
221
|
+
): string => {
|
|
222
|
+
if (parseFloat(event.actual_amount) === 0) {
|
|
223
|
+
return `Airtime transfer failed`;
|
|
224
|
+
}
|
|
225
|
+
return `Transferred **${event.actual_amount}** ${event.currency} of airtime`;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const renderCallStartedEvent = (): string => {
|
|
229
|
+
return `Call Started`;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const renderContactLanguageChangedEvent = (
|
|
233
|
+
event: ContactLanguageChangedEvent
|
|
234
|
+
): string => {
|
|
235
|
+
return `Language updated to **${event.language}**`;
|
|
236
|
+
};
|
|
8
237
|
|
|
9
|
-
const
|
|
238
|
+
export const renderOptinRequested = (event: OptinRequestedEvent): string => {
|
|
239
|
+
return `Requested opt-in for ${event.optin.name}`;
|
|
240
|
+
};
|
|
10
241
|
|
|
11
242
|
export class ContactChat extends ContactStoreElement {
|
|
12
243
|
public static get styles() {
|
|
@@ -38,7 +269,7 @@ export class ContactChat extends ContactStoreElement {
|
|
|
38
269
|
}
|
|
39
270
|
|
|
40
271
|
.chatbox {
|
|
41
|
-
|
|
272
|
+
background: #fff;
|
|
42
273
|
display: flex;
|
|
43
274
|
flex-direction: column;
|
|
44
275
|
--textarea-min-height: 1em;
|
|
@@ -46,10 +277,6 @@ export class ContactChat extends ContactStoreElement {
|
|
|
46
277
|
--widget-box-shadow-focused: none;
|
|
47
278
|
}
|
|
48
279
|
|
|
49
|
-
.chatbox:focus-within {
|
|
50
|
-
--textarea-height: 4em;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
280
|
.chatbox.full {
|
|
54
281
|
border-bottom-right-radius: 0 !important;
|
|
55
282
|
}
|
|
@@ -84,6 +311,11 @@ export class ContactChat extends ContactStoreElement {
|
|
|
84
311
|
--color-focus: transparent;
|
|
85
312
|
--color-widget-bg-focused: transparent;
|
|
86
313
|
}
|
|
314
|
+
|
|
315
|
+
.border {
|
|
316
|
+
border-top: 1px solid #f1f1f1;
|
|
317
|
+
margin: 0 1em;
|
|
318
|
+
}
|
|
87
319
|
`;
|
|
88
320
|
}
|
|
89
321
|
|
|
@@ -99,9 +331,6 @@ export class ContactChat extends ContactStoreElement {
|
|
|
99
331
|
@property({ type: Boolean })
|
|
100
332
|
showDetails = true;
|
|
101
333
|
|
|
102
|
-
@property({ type: Boolean })
|
|
103
|
-
monitor = false;
|
|
104
|
-
|
|
105
334
|
@property({ type: Object })
|
|
106
335
|
currentTicket: Ticket = null;
|
|
107
336
|
|
|
@@ -111,47 +340,40 @@ export class ContactChat extends ContactStoreElement {
|
|
|
111
340
|
@property({ type: String })
|
|
112
341
|
agent = '';
|
|
113
342
|
|
|
343
|
+
@property({ type: Boolean })
|
|
344
|
+
blockFetching = false;
|
|
345
|
+
|
|
346
|
+
@property({ type: String })
|
|
347
|
+
avatar = DEFAULT_AVATAR;
|
|
348
|
+
|
|
114
349
|
// http promise to monitor for completeness
|
|
115
350
|
public httpComplete: Promise<void>;
|
|
351
|
+
private chat: Chat;
|
|
352
|
+
|
|
353
|
+
ticket = null;
|
|
354
|
+
lastEventTime = null;
|
|
355
|
+
newestEventTime = null;
|
|
356
|
+
refreshId = null;
|
|
357
|
+
polling = false;
|
|
116
358
|
|
|
117
359
|
constructor() {
|
|
118
360
|
super();
|
|
119
361
|
}
|
|
120
362
|
|
|
121
|
-
|
|
363
|
+
public firstUpdated(
|
|
364
|
+
changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
365
|
+
): void {
|
|
366
|
+
super.firstUpdated(changed);
|
|
367
|
+
}
|
|
122
368
|
|
|
123
369
|
public connectedCallback() {
|
|
124
370
|
super.connectedCallback();
|
|
125
|
-
|
|
126
|
-
this.refreshInterval = setInterval(() => {
|
|
127
|
-
if (this.currentTicket && this.currentTicket.closed_on) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
this.refresh();
|
|
131
|
-
}, DEFAULT_REFRESH);
|
|
132
|
-
}
|
|
371
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
133
372
|
}
|
|
134
373
|
|
|
135
374
|
public disconnectedCallback() {
|
|
136
|
-
if (this.
|
|
137
|
-
clearInterval(this.
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
public getContactHistory(): ContactHistory {
|
|
142
|
-
return this.shadowRoot.querySelector(
|
|
143
|
-
'temba-contact-history'
|
|
144
|
-
) as ContactHistory;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
public refresh(scrollToBottom = false): void {
|
|
148
|
-
const contactHistory = this.getContactHistory();
|
|
149
|
-
if (contactHistory) {
|
|
150
|
-
if (scrollToBottom) {
|
|
151
|
-
contactHistory.scrollToBottom();
|
|
152
|
-
}
|
|
153
|
-
contactHistory.refresh();
|
|
154
|
-
// super.refresh();
|
|
375
|
+
if (this.refreshId) {
|
|
376
|
+
clearInterval(this.refreshId);
|
|
155
377
|
}
|
|
156
378
|
}
|
|
157
379
|
|
|
@@ -165,6 +387,25 @@ export class ContactChat extends ContactStoreElement {
|
|
|
165
387
|
) {
|
|
166
388
|
this.currentContact = this.data;
|
|
167
389
|
}
|
|
390
|
+
|
|
391
|
+
if (changedProperties.has('currentContact')) {
|
|
392
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
393
|
+
this.reset();
|
|
394
|
+
this.fetchPreviousMessages();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private reset() {
|
|
399
|
+
this.blockFetching = false;
|
|
400
|
+
this.ticket = null;
|
|
401
|
+
this.lastEventTime = null;
|
|
402
|
+
this.newestEventTime = null;
|
|
403
|
+
this.refreshId = null;
|
|
404
|
+
this.polling = false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public refresh() {
|
|
408
|
+
this.checkForNewMessages();
|
|
168
409
|
}
|
|
169
410
|
|
|
170
411
|
private handleSend(evt: CustomEvent) {
|
|
@@ -196,8 +437,8 @@ export class ContactChat extends ContactStoreElement {
|
|
|
196
437
|
postJSON(`/api/v2/messages.json`, payload)
|
|
197
438
|
.then((response) => {
|
|
198
439
|
if (response.status < 400) {
|
|
440
|
+
this.checkForNewMessages();
|
|
199
441
|
compose.reset();
|
|
200
|
-
this.refresh(true);
|
|
201
442
|
this.fireCustomEvent(CustomEventType.MessageSent, { msg: payload });
|
|
202
443
|
} else if (response.status < 500) {
|
|
203
444
|
if (
|
|
@@ -250,15 +491,322 @@ export class ContactChat extends ContactStoreElement {
|
|
|
250
491
|
return html`${contactHistoryAndChatbox}`;
|
|
251
492
|
}
|
|
252
493
|
|
|
494
|
+
private getEndpoint() {
|
|
495
|
+
return `/contact/history/${this.currentContact.uuid}/?_format=json`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private scheduleRefresh() {
|
|
499
|
+
// knock five seconds off the newest event time so we are
|
|
500
|
+
// a little more aggressive about refreshing short term
|
|
501
|
+
let window = new Date().getTime() - this.newestEventTime / 1000 - 5000;
|
|
502
|
+
|
|
503
|
+
if (this.refreshId) {
|
|
504
|
+
clearTimeout(this.refreshId);
|
|
505
|
+
this.refreshId = null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// wait no longer than 15 seconds
|
|
509
|
+
window = Math.min(window, 15000);
|
|
510
|
+
|
|
511
|
+
// wait at least 2 seconds
|
|
512
|
+
window = Math.max(window, 2000);
|
|
513
|
+
|
|
514
|
+
this.refreshId = setTimeout(() => {
|
|
515
|
+
this.checkForNewMessages();
|
|
516
|
+
}, window);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
public getEventMessage(event: ContactEvent): ChatEvent {
|
|
520
|
+
let message = null;
|
|
521
|
+
switch (event.type) {
|
|
522
|
+
case Events.ERROR:
|
|
523
|
+
case Events.FAILURE:
|
|
524
|
+
message = {
|
|
525
|
+
type: MessageType.Inline,
|
|
526
|
+
text: `Error during flow: ${toTitleCase(
|
|
527
|
+
(event as ErrorMessageEvent).text
|
|
528
|
+
)}`
|
|
529
|
+
};
|
|
530
|
+
break;
|
|
531
|
+
case Events.TICKET_OPENED:
|
|
532
|
+
message = {
|
|
533
|
+
type: MessageType.Inline,
|
|
534
|
+
text: renderTicketAction(event as TicketEvent, 'opened')
|
|
535
|
+
};
|
|
536
|
+
break;
|
|
537
|
+
case Events.TICKET_ASSIGNED:
|
|
538
|
+
message = {
|
|
539
|
+
type: MessageType.Inline,
|
|
540
|
+
text: renderTicketAssigned(event as TicketEvent)
|
|
541
|
+
};
|
|
542
|
+
break;
|
|
543
|
+
case Events.TICKET_REOPENED:
|
|
544
|
+
message = {
|
|
545
|
+
type: MessageType.Inline,
|
|
546
|
+
text: renderTicketAction(event as TicketEvent, 'reopened')
|
|
547
|
+
};
|
|
548
|
+
break;
|
|
549
|
+
case Events.TICKET_CLOSED:
|
|
550
|
+
message = {
|
|
551
|
+
type: MessageType.Inline,
|
|
552
|
+
text: renderTicketAction(event as TicketEvent, 'closed')
|
|
553
|
+
};
|
|
554
|
+
break;
|
|
555
|
+
case Events.FLOW_ENTERED:
|
|
556
|
+
case Events.FLOW_EXITED:
|
|
557
|
+
message = {
|
|
558
|
+
type: MessageType.Inline,
|
|
559
|
+
text: renderFlowEvent(event as FlowEvent)
|
|
560
|
+
};
|
|
561
|
+
break;
|
|
562
|
+
case Events.RUN_RESULT_CHANGED:
|
|
563
|
+
message = {
|
|
564
|
+
type: MessageType.Inline,
|
|
565
|
+
text: renderResultEvent(event as UpdateResultEvent)
|
|
566
|
+
};
|
|
567
|
+
break;
|
|
568
|
+
case Events.CONTACT_FIELD_CHANGED:
|
|
569
|
+
message = {
|
|
570
|
+
type: MessageType.Inline,
|
|
571
|
+
text: renderUpdateEvent(event as UpdateFieldEvent)
|
|
572
|
+
};
|
|
573
|
+
break;
|
|
574
|
+
case Events.CONTACT_NAME_CHANGED:
|
|
575
|
+
message = {
|
|
576
|
+
type: MessageType.Inline,
|
|
577
|
+
text: renderNameChanged(event as NameChangedEvent)
|
|
578
|
+
};
|
|
579
|
+
break;
|
|
580
|
+
case Events.CONTACT_URNS_CHANGED:
|
|
581
|
+
message = {
|
|
582
|
+
type: MessageType.Inline,
|
|
583
|
+
text: renderContactURNsChanged(event as URNsChangedEvent)
|
|
584
|
+
};
|
|
585
|
+
break;
|
|
586
|
+
case Events.EMAIL_SENT:
|
|
587
|
+
message = {
|
|
588
|
+
type: MessageType.Inline,
|
|
589
|
+
text: renderEmailSent(event as EmailSentEvent)
|
|
590
|
+
};
|
|
591
|
+
break;
|
|
592
|
+
case Events.INPUT_LABELS_ADDED:
|
|
593
|
+
message = {
|
|
594
|
+
type: MessageType.Inline,
|
|
595
|
+
text: renderLabelsAdded(event as LabelsAddedEvent)
|
|
596
|
+
};
|
|
597
|
+
break;
|
|
598
|
+
case Events.CONTACT_GROUPS_CHANGED:
|
|
599
|
+
message = {
|
|
600
|
+
type: MessageType.Inline,
|
|
601
|
+
text: renderContactGroupsEvent(event as ContactGroupsEvent)
|
|
602
|
+
};
|
|
603
|
+
break;
|
|
604
|
+
case Events.WEBHOOK_CALLED:
|
|
605
|
+
message = {
|
|
606
|
+
type: MessageType.Inline,
|
|
607
|
+
text: renderWebhookEvent(event as WebhookEvent)
|
|
608
|
+
};
|
|
609
|
+
break;
|
|
610
|
+
case Events.AIRTIME_TRANSFERRED:
|
|
611
|
+
message = {
|
|
612
|
+
type: MessageType.Inline,
|
|
613
|
+
text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
|
|
614
|
+
};
|
|
615
|
+
break;
|
|
616
|
+
case Events.CALL_STARTED:
|
|
617
|
+
message = {
|
|
618
|
+
type: MessageType.Inline,
|
|
619
|
+
text: renderCallStartedEvent()
|
|
620
|
+
};
|
|
621
|
+
break;
|
|
622
|
+
case Events.CAMPAIGN_FIRED:
|
|
623
|
+
message = {
|
|
624
|
+
type: MessageType.Inline,
|
|
625
|
+
text: renderCampaignFiredEvent(event as CampaignFiredEvent)
|
|
626
|
+
};
|
|
627
|
+
break;
|
|
628
|
+
case Events.CHANNEL_EVENT:
|
|
629
|
+
message = {
|
|
630
|
+
type: MessageType.Inline,
|
|
631
|
+
text: renderChannelEvent(event as ChannelEvent)
|
|
632
|
+
};
|
|
633
|
+
break;
|
|
634
|
+
case Events.CONTACT_LANGUAGE_CHANGED:
|
|
635
|
+
message = {
|
|
636
|
+
type: MessageType.Inline,
|
|
637
|
+
text: renderContactLanguageChangedEvent(
|
|
638
|
+
event as ContactLanguageChangedEvent
|
|
639
|
+
)
|
|
640
|
+
};
|
|
641
|
+
break;
|
|
642
|
+
case Events.OPTIN_REQUESTED:
|
|
643
|
+
message = {
|
|
644
|
+
type: MessageType.Inline,
|
|
645
|
+
text: renderOptinRequested(event as OptinRequestedEvent)
|
|
646
|
+
};
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
message.date = new Date(event.created_on);
|
|
651
|
+
return message;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private getUserForEvent(event: MsgEvent | TicketEvent) {
|
|
655
|
+
let user = null;
|
|
656
|
+
if (event.created_by) {
|
|
657
|
+
const storeUser = this.store.getUser(event.created_by.email);
|
|
658
|
+
if (storeUser) {
|
|
659
|
+
user = {
|
|
660
|
+
email: event.created_by.email,
|
|
661
|
+
name: [storeUser.first_name, storeUser.last_name].join(' '),
|
|
662
|
+
avatar: storeUser.avatar
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
} else if (event.type === 'msg_received') {
|
|
666
|
+
user = {
|
|
667
|
+
name: this.currentContact.name
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return user;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private createMessages(page: ContactHistoryPage): ChatEvent[] {
|
|
674
|
+
let messages = page.events.map((event) => {
|
|
675
|
+
const ts = new Date(event.created_on).getTime() * 1000;
|
|
676
|
+
if (ts > this.newestEventTime) {
|
|
677
|
+
this.newestEventTime = ts;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (event.type === 'ticket_note_added') {
|
|
681
|
+
const ticketEvent = event as TicketEvent;
|
|
682
|
+
return {
|
|
683
|
+
type: MessageType.Note,
|
|
684
|
+
id: event.created_on + event.type,
|
|
685
|
+
user: this.getUserForEvent(ticketEvent),
|
|
686
|
+
date: new Date(ticketEvent.created_on),
|
|
687
|
+
text: ticketEvent.note
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (event.type === 'msg_created' || event.type === 'msg_received') {
|
|
692
|
+
const msgEvent = event as MsgEvent;
|
|
693
|
+
return {
|
|
694
|
+
type: msgEvent.type === 'msg_created' ? 'msg_out' : 'msg_in',
|
|
695
|
+
id: msgEvent.msg.id + '',
|
|
696
|
+
user: this.getUserForEvent(msgEvent),
|
|
697
|
+
date: new Date(msgEvent.created_on),
|
|
698
|
+
attachments: msgEvent.msg.attachments,
|
|
699
|
+
text: msgEvent.msg.text,
|
|
700
|
+
sendError: msgEvent.status === 'E' || msgEvent.status === 'F',
|
|
701
|
+
popup: html`<div
|
|
702
|
+
style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
|
|
703
|
+
>
|
|
704
|
+
<div style="justify-content:left;text-align:left">
|
|
705
|
+
<temba-date
|
|
706
|
+
value=${msgEvent.created_on}
|
|
707
|
+
display="duration"
|
|
708
|
+
></temba-date>
|
|
709
|
+
|
|
710
|
+
${msgEvent.failed_reason_display
|
|
711
|
+
? html`
|
|
712
|
+
<div
|
|
713
|
+
style="margin-top:0.2em;margin-right: 0.5em;min-width:10em;max-width:15em;color:var(--color-error);font-size:0.9em"
|
|
714
|
+
>
|
|
715
|
+
${msgEvent.failed_reason_display}
|
|
716
|
+
</div>
|
|
717
|
+
`
|
|
718
|
+
: null}
|
|
719
|
+
</div>
|
|
720
|
+
${msgEvent.logs_url
|
|
721
|
+
? html`<a style="margin-left:0.5em" href="${msgEvent.logs_url}"
|
|
722
|
+
><temba-icon name="log"></temba-icon
|
|
723
|
+
></a>`
|
|
724
|
+
: null}
|
|
725
|
+
</div> `
|
|
726
|
+
};
|
|
727
|
+
} else {
|
|
728
|
+
return this.getEventMessage(event);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// remove any messages we don't recognize
|
|
733
|
+
messages = messages.filter((msg) => !!msg);
|
|
734
|
+
return messages as ChatEvent[];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private checkForNewMessages() {
|
|
738
|
+
// we are already working on it
|
|
739
|
+
if (this.polling) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const chat = this.chat;
|
|
744
|
+
const contactChat = this;
|
|
745
|
+
if (this.currentContact && this.newestEventTime) {
|
|
746
|
+
this.polling = true;
|
|
747
|
+
const endpoint = this.getEndpoint();
|
|
748
|
+
|
|
749
|
+
fetchContactHistory(
|
|
750
|
+
false,
|
|
751
|
+
endpoint,
|
|
752
|
+
this.currentTicket?.uuid,
|
|
753
|
+
null,
|
|
754
|
+
this.newestEventTime
|
|
755
|
+
).then((page: ContactHistoryPage) => {
|
|
756
|
+
this.lastEventTime = page.next_before;
|
|
757
|
+
const messages = this.createMessages(page);
|
|
758
|
+
if (messages.length === 0) {
|
|
759
|
+
contactChat.blockFetching = true;
|
|
760
|
+
}
|
|
761
|
+
messages.reverse();
|
|
762
|
+
chat.addMessages(messages, null, true);
|
|
763
|
+
this.polling = false;
|
|
764
|
+
this.scheduleRefresh();
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private fetchPreviousMessages() {
|
|
770
|
+
const chat = this.chat;
|
|
771
|
+
const contactChat = this;
|
|
772
|
+
|
|
773
|
+
if (!chat || chat.fetching || contactChat.blockFetching) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
chat.fetching = true;
|
|
778
|
+
if (this.currentContact) {
|
|
779
|
+
const endpoint = this.getEndpoint();
|
|
780
|
+
fetchContactHistory(
|
|
781
|
+
false,
|
|
782
|
+
endpoint,
|
|
783
|
+
this.currentTicket?.uuid,
|
|
784
|
+
this.lastEventTime
|
|
785
|
+
).then((page: ContactHistoryPage) => {
|
|
786
|
+
this.lastEventTime = page.next_before;
|
|
787
|
+
const messages = this.createMessages(page);
|
|
788
|
+
messages.reverse();
|
|
789
|
+
|
|
790
|
+
if (messages.length === 0) {
|
|
791
|
+
contactChat.blockFetching = true;
|
|
792
|
+
}
|
|
793
|
+
chat.addMessages(messages);
|
|
794
|
+
this.scheduleRefresh();
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private fetchComplete() {
|
|
800
|
+
this.chat.fetching = false;
|
|
801
|
+
}
|
|
802
|
+
|
|
253
803
|
private getTembaContactHistory(): TemplateResult {
|
|
254
|
-
return html
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
>
|
|
261
|
-
</temba-contact-history>`;
|
|
804
|
+
return html`<temba-chat
|
|
805
|
+
@temba-scroll-threshold=${this.fetchPreviousMessages}
|
|
806
|
+
@temba-fetch-complete=${this.fetchComplete}
|
|
807
|
+
avatar=${this.avatar}
|
|
808
|
+
agent
|
|
809
|
+
></temba-chat>`;
|
|
262
810
|
}
|
|
263
811
|
|
|
264
812
|
private getTembaChatbox(): TemplateResult {
|
|
@@ -286,15 +834,17 @@ export class ContactChat extends ContactStoreElement {
|
|
|
286
834
|
}
|
|
287
835
|
|
|
288
836
|
private getChatbox(): TemplateResult {
|
|
289
|
-
return html`<div class="
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
837
|
+
return html`<div class="border"></div>
|
|
838
|
+
<div class="chatbox">
|
|
839
|
+
<temba-compose
|
|
840
|
+
chatbox
|
|
841
|
+
attachments
|
|
842
|
+
counter
|
|
843
|
+
button
|
|
844
|
+
autogrow
|
|
845
|
+
@temba-button-clicked=${this.handleSend.bind(this)}
|
|
846
|
+
>
|
|
847
|
+
</temba-compose>
|
|
848
|
+
</div>`;
|
|
299
849
|
}
|
|
300
850
|
}
|