@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.
@@ -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 className={styles.consequence}>
205
- {internal
206
- ? 'Only staff can see this.'
207
- : 'This is emailed to the customer.'}
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,