@visns-studio/visns-components 6.28.0 → 6.31.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/README.md +123 -0
- package/package.json +1 -1
- package/src/components/callQueue/CallQueueDiagnostics.jsx +13 -0
- package/src/components/callQueue/CallQueuePop.jsx +308 -43
- package/src/components/callQueue/callPopStatus.js +23 -0
- package/src/components/callQueue/callQueueHelpers.js +66 -0
- package/src/components/echo/sharedPrivateChannel.js +217 -0
- package/src/components/phone/ZoomPhoneBadge.jsx +5 -0
- package/src/components/phone/phonePresenceHelpers.js +31 -0
- package/src/components/phone/useZoomPhoneLive.js +147 -7
- package/src/components/styles/TicketConversation.module.scss +22 -1
- package/src/components/tickets/TicketConversation.jsx +96 -5
- package/src/index.js +15 -0
|
@@ -25,6 +25,44 @@ const CHANNEL_ICON = {
|
|
|
25
25
|
Web: Globe,
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Who an email went to, or is about to go to.
|
|
30
|
+
*
|
|
31
|
+
* A reply on a ticket goes to everybody on the chain, not to the one person
|
|
32
|
+
* whose message is on screen — so the composer has to SAY so before the button
|
|
33
|
+
* is pressed, and a sent entry has to say who it reached. The same two
|
|
34
|
+
* readings are used in both places, which is why they live here rather than
|
|
35
|
+
* once in the Composer and again in Entry:
|
|
36
|
+
*
|
|
37
|
+
* names() — what is drawn. "Dana Reid, accounts@acme.test" — the name
|
|
38
|
+
* where there is one, the address where there is not.
|
|
39
|
+
* addresses() — what is HOVERED. The full `Name <address>` list, because
|
|
40
|
+
* two people called Dana is exactly the case where the name
|
|
41
|
+
* alone is not enough to check a send against.
|
|
42
|
+
*
|
|
43
|
+
* Both take a possibly-absent list: `recipients` is null on an internal note
|
|
44
|
+
* and on every row imported before the server knew any of this.
|
|
45
|
+
*/
|
|
46
|
+
const names = (list) =>
|
|
47
|
+
(Array.isArray(list) ? list : [])
|
|
48
|
+
.map((r) => r.name || r.address)
|
|
49
|
+
.join(', ');
|
|
50
|
+
|
|
51
|
+
const addresses = (list) =>
|
|
52
|
+
(Array.isArray(list) ? list : [])
|
|
53
|
+
.map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))
|
|
54
|
+
.join(', ');
|
|
55
|
+
|
|
56
|
+
/** The `title` for a To/Cc line — undefined rather than an empty attribute. */
|
|
57
|
+
const recipientTitle = (to, cc) => {
|
|
58
|
+
const lines = [];
|
|
59
|
+
|
|
60
|
+
if (to.length) lines.push(`To: ${addresses(to)}`);
|
|
61
|
+
if (cc.length) lines.push(`Cc: ${addresses(cc)}`);
|
|
62
|
+
|
|
63
|
+
return lines.length ? lines.join('\n') : undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
28
66
|
/**
|
|
29
67
|
* A support ticket's conversation.
|
|
30
68
|
*
|
|
@@ -49,6 +87,10 @@ const TicketConversation = ({
|
|
|
49
87
|
}) => {
|
|
50
88
|
const [entries, setEntries] = useState([]);
|
|
51
89
|
const [ticket, setTicket] = useState(null);
|
|
90
|
+
// Who a reply would go to. null = the server does not know, or the ticket
|
|
91
|
+
// cannot be answered by email — the composer falls back to the old
|
|
92
|
+
// sentence rather than naming nobody.
|
|
93
|
+
const [audience, setAudience] = useState(null);
|
|
52
94
|
const [loading, setLoading] = useState(true);
|
|
53
95
|
const [failed, setFailed] = useState(false);
|
|
54
96
|
const mountedRef = useRef(true);
|
|
@@ -72,6 +114,7 @@ const TicketConversation = ({
|
|
|
72
114
|
|
|
73
115
|
setEntries(Array.isArray(result?.data) ? result.data : []);
|
|
74
116
|
setTicket(result?.ticket ?? null);
|
|
117
|
+
setAudience(result?.reply_audience ?? null);
|
|
75
118
|
setLoading(false);
|
|
76
119
|
onLoaded?.(result?.ticket ?? null);
|
|
77
120
|
},
|
|
@@ -128,6 +171,7 @@ const TicketConversation = ({
|
|
|
128
171
|
<Composer
|
|
129
172
|
base={base}
|
|
130
173
|
ticketId={ticketId}
|
|
174
|
+
audience={audience}
|
|
131
175
|
onPosted={(entry) => setEntries((prev) => [entry, ...prev])}
|
|
132
176
|
/>
|
|
133
177
|
)}
|
|
@@ -155,8 +199,15 @@ const TicketConversation = ({
|
|
|
155
199
|
* this" / "only staff can see this") every time. An agent sending an internal
|
|
156
200
|
* note to a customer is the expensive mistake here, and a toggle whose state
|
|
157
201
|
* you have to remember is how it happens.
|
|
202
|
+
*
|
|
203
|
+
* `audience` is the second half of that argument. A reply goes to everybody on
|
|
204
|
+
* the email chain — which on a forwarded ticket is regularly somebody the agent
|
|
205
|
+
* has never seen — so the consequence NAMES them rather than saying "the
|
|
206
|
+
* customer". Nothing is truncated to "+3 more": the whole point of the line is
|
|
207
|
+
* to be read before sending, and the name that would be hidden is the one worth
|
|
208
|
+
* noticing. `null` means the server could not say, and the old sentence stands.
|
|
158
209
|
*/
|
|
159
|
-
const Composer = ({ base, ticketId, onPosted }) => {
|
|
210
|
+
const Composer = ({ base, ticketId, audience, onPosted }) => {
|
|
160
211
|
const [body, setBody] = useState('');
|
|
161
212
|
const [internal, setInternal] = useState(false);
|
|
162
213
|
const [sending, setSending] = useState(false);
|
|
@@ -189,6 +240,22 @@ const Composer = ({ base, ticketId, onPosted }) => {
|
|
|
189
240
|
});
|
|
190
241
|
};
|
|
191
242
|
|
|
243
|
+
const to = audience?.to ?? [];
|
|
244
|
+
const cc = audience?.cc ?? [];
|
|
245
|
+
const named = !internal && to.length > 0;
|
|
246
|
+
|
|
247
|
+
let consequence;
|
|
248
|
+
|
|
249
|
+
if (internal) {
|
|
250
|
+
consequence = 'Only staff can see this.';
|
|
251
|
+
} else if (named) {
|
|
252
|
+
consequence = `Emailed to ${names(to)}`;
|
|
253
|
+
|
|
254
|
+
if (cc.length) consequence += ` · Cc ${names(cc)}`;
|
|
255
|
+
} else {
|
|
256
|
+
consequence = 'This is emailed to the customer.';
|
|
257
|
+
}
|
|
258
|
+
|
|
192
259
|
return (
|
|
193
260
|
<div className={`${styles.composer} ${internal ? styles.composerNote : ''}`}>
|
|
194
261
|
<div className={styles.composerBar}>
|
|
@@ -201,10 +268,11 @@ const Composer = ({ base, ticketId, onPosted }) => {
|
|
|
201
268
|
Internal note
|
|
202
269
|
</label>
|
|
203
270
|
|
|
204
|
-
<span
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
271
|
+
<span
|
|
272
|
+
className={styles.consequence}
|
|
273
|
+
title={named ? recipientTitle(to, cc) : undefined}
|
|
274
|
+
>
|
|
275
|
+
{consequence}
|
|
208
276
|
</span>
|
|
209
277
|
</div>
|
|
210
278
|
|
|
@@ -408,6 +476,20 @@ const Entry = ({ entry }) => {
|
|
|
408
476
|
? styles.entryIn
|
|
409
477
|
: styles.entryOut;
|
|
410
478
|
|
|
479
|
+
/* Who else was on this message. Drawn for BOTH directions, and they answer
|
|
480
|
+
different questions: on our reply it is who we sent to, on the
|
|
481
|
+
customer's it is who else at their end has already read it — which is
|
|
482
|
+
what tells an agent whether the answer needs to reach anybody new.
|
|
483
|
+
Absent on notes, and on every row that predates the server sending it. */
|
|
484
|
+
const to = entry.recipients?.to ?? [];
|
|
485
|
+
const cc = entry.recipients?.cc ?? [];
|
|
486
|
+
const parts = [];
|
|
487
|
+
|
|
488
|
+
if (to.length) parts.push(`To ${names(to)}`);
|
|
489
|
+
if (cc.length) parts.push(`Cc ${names(cc)}`);
|
|
490
|
+
|
|
491
|
+
const showRecipients = !isNote && parts.length > 0;
|
|
492
|
+
|
|
411
493
|
return (
|
|
412
494
|
<li className={`${styles.entry} ${cls}`}>
|
|
413
495
|
<div className={styles.entryHead}>
|
|
@@ -452,6 +534,15 @@ const Entry = ({ entry }) => {
|
|
|
452
534
|
)}
|
|
453
535
|
</div>
|
|
454
536
|
|
|
537
|
+
{showRecipients && (
|
|
538
|
+
<p
|
|
539
|
+
className={styles.recipients}
|
|
540
|
+
title={recipientTitle(to, cc)}
|
|
541
|
+
>
|
|
542
|
+
{parts.join(' · ')}
|
|
543
|
+
</p>
|
|
544
|
+
)}
|
|
545
|
+
|
|
455
546
|
<EntryBody entry={entry} />
|
|
456
547
|
</li>
|
|
457
548
|
);
|
package/src/index.js
CHANGED
|
@@ -151,9 +151,20 @@ import {
|
|
|
151
151
|
import {
|
|
152
152
|
CALL_POP_STATUS_EVENT,
|
|
153
153
|
getCallPopStatus,
|
|
154
|
+
noteCallPopChannelPresent,
|
|
154
155
|
updateCallPopStatus,
|
|
155
156
|
} from './components/callQueue/callPopStatus';
|
|
156
157
|
|
|
158
|
+
// Ref-counted `echo.private()`. Exported because any host component that
|
|
159
|
+
// listens on a channel one of ours also listens on must go through it —
|
|
160
|
+
// laravel-echo's `leave()` destroys a channel for every holder, not just the
|
|
161
|
+
// caller, which is how the call pop lost its subscription to the header badge.
|
|
162
|
+
import {
|
|
163
|
+
acquirePrivateChannel,
|
|
164
|
+
hasPrivateChannel,
|
|
165
|
+
releasePrivateChannel,
|
|
166
|
+
} from './components/echo/sharedPrivateChannel';
|
|
167
|
+
|
|
157
168
|
// Vault — the staff password manager. VaultQuickSearch mounts through
|
|
158
169
|
// Navigation's `renderers` slot; VaultManager is the page behind it.
|
|
159
170
|
import OtpChip from './components/vault/OtpChip';
|
|
@@ -376,7 +387,11 @@ export {
|
|
|
376
387
|
clientDetails,
|
|
377
388
|
demoClientDetails,
|
|
378
389
|
getCallPopStatus,
|
|
390
|
+
noteCallPopChannelPresent,
|
|
379
391
|
updateCallPopStatus,
|
|
392
|
+
acquirePrivateChannel,
|
|
393
|
+
hasPrivateChannel,
|
|
394
|
+
releasePrivateChannel,
|
|
380
395
|
cleanBase32,
|
|
381
396
|
decodeFromBlob,
|
|
382
397
|
decodeFromSource,
|