@visns-studio/visns-components 6.6.3 → 6.15.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 +1658 -18
- package/package.json +10 -4
- package/src/components/DataGrid.jsx +619 -158
- package/src/components/Fetch.jsx +80 -3
- package/src/components/Form.jsx +9 -0
- package/src/components/Navigation.jsx +109 -50
- package/src/components/Notification.jsx +279 -9
- package/src/components/TableFilter.jsx +14 -1
- package/src/components/auth/AuthBrandPanel.jsx +43 -0
- package/src/components/auth/AuthLoading.jsx +78 -0
- package/src/components/auth/AuthShell.jsx +59 -0
- package/src/components/auth/ClientAuth.jsx +161 -0
- package/src/components/auth/ClientAuthFrame.jsx +59 -0
- package/src/components/auth/ClientLogin.jsx +266 -55
- package/src/components/auth/ClientOTPVerify.jsx +587 -115
- package/src/components/auth/ImpersonateGate.jsx +254 -0
- package/src/components/auth/Login.jsx +134 -41
- package/src/components/auth/LogoutScreen.jsx +112 -0
- package/src/components/auth/Reset.jsx +134 -77
- package/src/components/auth/TwoFactorAuth.jsx +475 -297
- package/src/components/auth/Verify.jsx +237 -126
- package/src/components/auth/authEndpoints.js +105 -0
- package/src/components/auth/authFont.js +23 -0
- package/src/components/auth/authHelpers.js +465 -0
- package/src/components/auth/clientAuthProtocols.js +240 -0
- package/src/components/auth/useOptionalRouter.js +49 -0
- package/src/components/callQueue/CallQueuePop.jsx +1502 -0
- package/src/components/callQueue/CallQueueSettings.jsx +508 -0
- package/src/components/callQueue/callQueueHelpers.js +280 -0
- package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
- package/src/components/columns/AutoGrowCell.jsx +141 -0
- package/src/components/columns/ColumnRenderers.jsx +53 -7
- package/src/components/generic/GenericAuth.jsx +163 -96
- package/src/components/generic/GenericDetail.jsx +215 -90
- package/src/components/generic/GenericIndex.jsx +20 -47
- package/src/components/generic/GenericMain.jsx +5 -0
- package/src/components/generic/GroupedReportRenderer.jsx +1 -5
- package/src/components/generic/StandardModal.jsx +17 -5
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
- package/src/components/navActive.js +60 -0
- package/src/components/notify/desktopNotifications.js +256 -0
- package/src/components/sketch/SketchField.jsx +12 -2
- package/src/components/sms/SmsComposeModal.jsx +495 -0
- package/src/components/sms/SmsInbox.jsx +596 -0
- package/src/components/sms/SmsInboxBadge.jsx +468 -0
- package/src/components/sms/SmsLineSettings.jsx +854 -0
- package/src/components/sms/SmsThreadPanel.jsx +1184 -0
- package/src/components/sms/smsEndpoints.js +56 -0
- package/src/components/sms/smsHelpers.js +1075 -0
- package/src/components/sms/smsLiveState.js +132 -0
- package/src/components/sms/useSmsLive.js +307 -0
- package/src/components/styles/CallQueuePop.module.scss +646 -0
- package/src/components/styles/CallQueueSettings.module.scss +460 -0
- package/src/components/styles/ClientAuth.module.scss +229 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/GenericDetail.module.scss +153 -26
- package/src/components/styles/GenericIndex.module.scss +13 -0
- package/src/components/styles/GenericMain.module.scss +19 -2
- package/src/components/styles/ImpersonateGate.module.scss +85 -0
- package/src/components/styles/Login.module.scss +13 -122
- package/src/components/styles/LogoutScreen.module.scss +89 -0
- package/src/components/styles/Navigation.module.scss +509 -223
- package/src/components/styles/Notification.module.scss +103 -0
- package/src/components/styles/Reset.module.scss +52 -186
- package/src/components/styles/Sms.module.scss +2382 -0
- package/src/components/styles/TableFilter.module.scss +118 -98
- package/src/components/styles/TwoFactorAuth.module.scss +115 -176
- package/src/components/styles/Vault.module.scss +1983 -0
- package/src/components/styles/Verify.module.scss +57 -180
- package/src/components/styles/_authBrandPanel.scss +163 -0
- package/src/components/styles/_authShell.scss +369 -0
- package/src/components/styles/global-datagrid.css +29 -0
- package/src/components/utils/buildEnv.js +67 -0
- package/src/components/utils/displayValue.js +94 -0
- package/src/components/utils/useDensity.js +345 -9
- package/src/components/vault/OtpChip.jsx +208 -0
- package/src/components/vault/PasswordGenerator.jsx +145 -0
- package/src/components/vault/QrScanner.jsx +326 -0
- package/src/components/vault/VaultAccessLog.jsx +225 -0
- package/src/components/vault/VaultConfirmPanel.jsx +113 -0
- package/src/components/vault/VaultEntryForm.jsx +740 -0
- package/src/components/vault/VaultManager.jsx +1000 -0
- package/src/components/vault/VaultQuickSearch.jsx +681 -0
- package/src/components/vault/qrDecode.js +257 -0
- package/src/components/vault/useDebouncedValue.js +22 -0
- package/src/components/vault/useVaultReveal.js +160 -0
- package/src/components/vault/vaultClipboard.js +33 -0
- package/src/components/vault/vaultEndpoints.js +40 -0
- package/src/components/vault/vaultFit.js +116 -0
- package/src/components/vault/vaultHelpers.js +547 -0
- package/src/components/vault/vaultNavigation.jsx +58 -0
- package/src/components/vault/vaultOtp.js +105 -0
- package/src/index.js +233 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Loader2, MessageSquare, Unplug, TriangleAlert } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import CustomFetch from '../Fetch';
|
|
5
|
+
import { notify as desktopNotify } from '../notify/desktopNotifications';
|
|
6
|
+
import styles from '../styles/Sms.module.scss';
|
|
7
|
+
// The HOC is named for the vault because that is where it was written, but
|
|
8
|
+
// nothing in it is vault-specific: it hands a component a `navigate` that
|
|
9
|
+
// works inside a Router and outside one. Imported rather than copied so there
|
|
10
|
+
// is one implementation of that decision in the library.
|
|
11
|
+
import withVaultNavigation from '../vault/vaultNavigation';
|
|
12
|
+
import { resolveSmsEndpoints } from './smsEndpoints';
|
|
13
|
+
import useSmsLive, { defaultChannelFor } from './useSmsLive';
|
|
14
|
+
import {
|
|
15
|
+
badgeCount,
|
|
16
|
+
initialsFor,
|
|
17
|
+
relativeTime,
|
|
18
|
+
threadDisplayName,
|
|
19
|
+
upsertThread,
|
|
20
|
+
} from './smsHelpers';
|
|
21
|
+
|
|
22
|
+
/** The one-line preview under a thread's name. */
|
|
23
|
+
const previewOf = (thread) => {
|
|
24
|
+
const message = thread?.last_message;
|
|
25
|
+
|
|
26
|
+
if (!message || !message.body) return 'No messages yet';
|
|
27
|
+
|
|
28
|
+
const body = String(message.body).replace(/\s+/g, ' ').trim();
|
|
29
|
+
|
|
30
|
+
return message.direction === 'out' ? `You: ${body}` : body;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Messages, from the header.
|
|
35
|
+
*
|
|
36
|
+
* Mounted as an account action through Navigation's `renderers` slot, so it
|
|
37
|
+
* sits in the chip row beside the notification bell and the vault and wears
|
|
38
|
+
* the same trigger. It answers one question — "has anyone texted us?" — and
|
|
39
|
+
* gives one path onward, into the inbox.
|
|
40
|
+
*
|
|
41
|
+
* The two states this was built around are the ones that do not exist yet:
|
|
42
|
+
* Zoom is not connected, and the dev transport that stands in for it. Both say
|
|
43
|
+
* so on the face of the popover rather than in a console somewhere, because a
|
|
44
|
+
* message that is queued and a message that is sent look identical from here
|
|
45
|
+
* otherwise.
|
|
46
|
+
*/
|
|
47
|
+
const SmsInboxBadgeInner = ({
|
|
48
|
+
setting,
|
|
49
|
+
// Accepted so the component matches the shape Navigation's renderers hand
|
|
50
|
+
// over, even though the popover does not read them.
|
|
51
|
+
userProfile: _userProfile,
|
|
52
|
+
setSystemAuth: _setSystemAuth,
|
|
53
|
+
endpoints,
|
|
54
|
+
echo = null,
|
|
55
|
+
channelFor = defaultChannelFor,
|
|
56
|
+
inboxUrl = '/sms',
|
|
57
|
+
limit = 8,
|
|
58
|
+
navigate,
|
|
59
|
+
}) => {
|
|
60
|
+
const routes = useMemo(() => resolveSmsEndpoints(endpoints), [endpoints]);
|
|
61
|
+
|
|
62
|
+
const [open, setOpen] = useState(false);
|
|
63
|
+
const [status, setStatus] = useState(null);
|
|
64
|
+
const [lines, setLines] = useState([]);
|
|
65
|
+
const [threads, setThreads] = useState([]);
|
|
66
|
+
const [unread, setUnread] = useState(0);
|
|
67
|
+
const [loading, setLoading] = useState(false);
|
|
68
|
+
const [loaded, setLoaded] = useState(false);
|
|
69
|
+
const [highlight, setHighlight] = useState(0);
|
|
70
|
+
|
|
71
|
+
const wrapperRef = useRef(null);
|
|
72
|
+
const triggerRef = useRef(null);
|
|
73
|
+
const listRef = useRef(null);
|
|
74
|
+
const mountedRef = useRef(true);
|
|
75
|
+
// Guards against an early response overwriting a later one.
|
|
76
|
+
const requestRef = useRef(0);
|
|
77
|
+
|
|
78
|
+
useEffect(
|
|
79
|
+
() => () => {
|
|
80
|
+
mountedRef.current = false;
|
|
81
|
+
},
|
|
82
|
+
[]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/* -------------------------------------------------------------- loading */
|
|
86
|
+
|
|
87
|
+
/** Status carries the unread total, so this is also the badge's source. */
|
|
88
|
+
const loadStatus = useCallback(() => {
|
|
89
|
+
CustomFetch(
|
|
90
|
+
routes.status,
|
|
91
|
+
'GET',
|
|
92
|
+
null,
|
|
93
|
+
(result) => {
|
|
94
|
+
if (!mountedRef.current || !result) return;
|
|
95
|
+
|
|
96
|
+
setStatus(result);
|
|
97
|
+
|
|
98
|
+
if (typeof result.unread_total === 'number') {
|
|
99
|
+
setUnread(result.unread_total);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
// Silent: a header chip must not toast at someone because a
|
|
103
|
+
// background poll failed.
|
|
104
|
+
() => {}
|
|
105
|
+
).catch(() => {});
|
|
106
|
+
}, [routes]);
|
|
107
|
+
|
|
108
|
+
const loadLines = useCallback(() => {
|
|
109
|
+
CustomFetch(
|
|
110
|
+
routes.lines,
|
|
111
|
+
'GET',
|
|
112
|
+
null,
|
|
113
|
+
(result) => {
|
|
114
|
+
if (!mountedRef.current) return;
|
|
115
|
+
|
|
116
|
+
setLines(Array.isArray(result?.data) ? result.data : []);
|
|
117
|
+
},
|
|
118
|
+
() => {}
|
|
119
|
+
).catch(() => {});
|
|
120
|
+
}, [routes]);
|
|
121
|
+
|
|
122
|
+
const loadThreads = useCallback(() => {
|
|
123
|
+
const ticket = requestRef.current + 1;
|
|
124
|
+
|
|
125
|
+
requestRef.current = ticket;
|
|
126
|
+
setLoading(true);
|
|
127
|
+
|
|
128
|
+
CustomFetch(
|
|
129
|
+
routes.threads,
|
|
130
|
+
'GET',
|
|
131
|
+
{ per_page: limit, page: 1 },
|
|
132
|
+
(result) => {
|
|
133
|
+
if (!mountedRef.current || requestRef.current !== ticket) return;
|
|
134
|
+
|
|
135
|
+
setThreads(Array.isArray(result?.data) ? result.data : []);
|
|
136
|
+
setLoading(false);
|
|
137
|
+
setLoaded(true);
|
|
138
|
+
setHighlight(0);
|
|
139
|
+
},
|
|
140
|
+
() => {
|
|
141
|
+
if (!mountedRef.current || requestRef.current !== ticket) return;
|
|
142
|
+
|
|
143
|
+
setLoading(false);
|
|
144
|
+
setLoaded(true);
|
|
145
|
+
}
|
|
146
|
+
).catch(() => {
|
|
147
|
+
if (!mountedRef.current || requestRef.current !== ticket) return;
|
|
148
|
+
|
|
149
|
+
setLoading(false);
|
|
150
|
+
setLoaded(true);
|
|
151
|
+
});
|
|
152
|
+
}, [routes, limit]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
loadStatus();
|
|
156
|
+
loadLines();
|
|
157
|
+
}, [loadStatus, loadLines]);
|
|
158
|
+
|
|
159
|
+
// The list is deliberately not cached between openings: a preview from
|
|
160
|
+
// twenty minutes ago is worse than a spinner.
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!open) return;
|
|
163
|
+
|
|
164
|
+
loadThreads();
|
|
165
|
+
}, [open, loadThreads]);
|
|
166
|
+
|
|
167
|
+
/* ----------------------------------------------------------------- live */
|
|
168
|
+
|
|
169
|
+
const handleReceived = useCallback(
|
|
170
|
+
(event) => {
|
|
171
|
+
if (!mountedRef.current) return;
|
|
172
|
+
|
|
173
|
+
if (event?.thread) {
|
|
174
|
+
setThreads((prev) => upsertThread(prev, event.thread).slice(0, 50));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// An inbound message is one more unread, unless the open conversation
|
|
178
|
+
// just marked it read — which the inbox page, not this popover, owns.
|
|
179
|
+
setUnread((prev) => prev + 1);
|
|
180
|
+
|
|
181
|
+
// ...and a desktop alert, if the user has asked for them and is not
|
|
182
|
+
// already looking at this tab. Only `sms.received`: an updated
|
|
183
|
+
// delivery status is not news anybody needs an OS toast for.
|
|
184
|
+
const thread = event?.thread;
|
|
185
|
+
const body = event?.message?.body;
|
|
186
|
+
|
|
187
|
+
if (!thread) return;
|
|
188
|
+
|
|
189
|
+
desktopNotify({
|
|
190
|
+
title: `New text from ${threadDisplayName(thread)}`,
|
|
191
|
+
body: String(body || '')
|
|
192
|
+
.replace(/\s+/g, ' ')
|
|
193
|
+
.trim()
|
|
194
|
+
.slice(0, 120),
|
|
195
|
+
// One toast per conversation: a burst of five replaces itself
|
|
196
|
+
// rather than burying the desktop.
|
|
197
|
+
tag: `sms-${thread.id}`,
|
|
198
|
+
url: `${inboxUrl}?thread=${thread.id}`,
|
|
199
|
+
navigate,
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
[inboxUrl, navigate]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const handleUpdated = useCallback((event) => {
|
|
206
|
+
if (!mountedRef.current || !event?.thread) return;
|
|
207
|
+
|
|
208
|
+
setThreads((prev) => upsertThread(prev, event.thread).slice(0, 50));
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
const handleUnread = useCallback((payload) => {
|
|
212
|
+
if (!mountedRef.current) return;
|
|
213
|
+
|
|
214
|
+
if (typeof payload?.total === 'number') setUnread(payload.total);
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
useSmsLive({
|
|
218
|
+
echo,
|
|
219
|
+
lines,
|
|
220
|
+
channelFor,
|
|
221
|
+
endpoints,
|
|
222
|
+
onReceived: handleReceived,
|
|
223
|
+
onUpdated: handleUpdated,
|
|
224
|
+
onUnread: handleUnread,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/* -------------------------------------------------------------- opening */
|
|
228
|
+
|
|
229
|
+
const close = useCallback(() => {
|
|
230
|
+
setOpen(false);
|
|
231
|
+
setHighlight(0);
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!open) return undefined;
|
|
236
|
+
|
|
237
|
+
const onMouseDown = (event) => {
|
|
238
|
+
if (wrapperRef.current?.contains(event.target)) return;
|
|
239
|
+
if (triggerRef.current?.contains(event.target)) return;
|
|
240
|
+
|
|
241
|
+
close();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
document.addEventListener('mousedown', onMouseDown);
|
|
245
|
+
|
|
246
|
+
return () => document.removeEventListener('mousedown', onMouseDown);
|
|
247
|
+
}, [open, close]);
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!open) return undefined;
|
|
251
|
+
|
|
252
|
+
// Focus after paint, or the list is not in the document yet. The
|
|
253
|
+
// popover has no search box, so the list itself takes the keyboard.
|
|
254
|
+
const handle = setTimeout(() => listRef.current?.focus(), 0);
|
|
255
|
+
|
|
256
|
+
return () => clearTimeout(handle);
|
|
257
|
+
}, [open]);
|
|
258
|
+
|
|
259
|
+
const openThread = useCallback(
|
|
260
|
+
(thread) => {
|
|
261
|
+
close();
|
|
262
|
+
navigate(thread ? `${inboxUrl}?thread=${thread.id}` : inboxUrl);
|
|
263
|
+
},
|
|
264
|
+
[close, navigate, inboxUrl]
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const onKeyDown = useCallback(
|
|
268
|
+
(event) => {
|
|
269
|
+
if (event.key === 'Escape') {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
close();
|
|
272
|
+
triggerRef.current?.focus();
|
|
273
|
+
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (threads.length === 0) return;
|
|
278
|
+
|
|
279
|
+
if (event.key === 'ArrowDown') {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
setHighlight((prev) => (prev + 1) % threads.length);
|
|
282
|
+
} else if (event.key === 'ArrowUp') {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
setHighlight((prev) => (prev - 1 + threads.length) % threads.length);
|
|
285
|
+
} else if (event.key === 'Enter') {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
openThread(threads[highlight]);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
[threads, highlight, close, openThread]
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
/* --------------------------------------------------------------- render */
|
|
294
|
+
|
|
295
|
+
const label = setting?.label || 'Messages';
|
|
296
|
+
const count = badgeCount(unread);
|
|
297
|
+
|
|
298
|
+
// Two different sentences, because they mean two different things: one is
|
|
299
|
+
// "this will send later", the other is "this is pretending to send".
|
|
300
|
+
const note = (() => {
|
|
301
|
+
if (!status) return null;
|
|
302
|
+
if (status.connected === false && status.transport === 'null') {
|
|
303
|
+
return 'Not connected to Zoom yet — messages you send are held';
|
|
304
|
+
}
|
|
305
|
+
if (status.transport === 'log') return 'Dev transport — replies are simulated';
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
})();
|
|
309
|
+
|
|
310
|
+
const renderRow = (thread, index) => {
|
|
311
|
+
const unreadCount = Number(thread.unread_count) || 0;
|
|
312
|
+
const name = threadDisplayName(thread);
|
|
313
|
+
const preview = previewOf(thread);
|
|
314
|
+
const stamp = thread.last_message?.at ?? thread.updated_at;
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<li
|
|
318
|
+
key={thread.id}
|
|
319
|
+
className={[
|
|
320
|
+
styles.row,
|
|
321
|
+
index === highlight ? styles.rowActive : '',
|
|
322
|
+
unreadCount > 0 ? styles.rowUnread : '',
|
|
323
|
+
]
|
|
324
|
+
.filter(Boolean)
|
|
325
|
+
.join(' ')}
|
|
326
|
+
role="option"
|
|
327
|
+
aria-selected={index === highlight}
|
|
328
|
+
onMouseEnter={() => setHighlight(index)}
|
|
329
|
+
onClick={() => openThread(thread)}
|
|
330
|
+
title={`${name} — ${preview}`}
|
|
331
|
+
>
|
|
332
|
+
{/* The unread marker, in its own fixed track. It used to be a
|
|
333
|
+
border-left, which is layout: unread rows sat three pixels
|
|
334
|
+
to the right of read ones and the avatars zig-zagged. */}
|
|
335
|
+
<span className={styles.rowAccent} aria-hidden="true" />
|
|
336
|
+
|
|
337
|
+
<span className={styles.avatar} aria-hidden="true">
|
|
338
|
+
{initialsFor(name)}
|
|
339
|
+
</span>
|
|
340
|
+
|
|
341
|
+
<span className={styles.rowMain}>
|
|
342
|
+
<strong className={styles.rowName}>{name}</strong>
|
|
343
|
+
<span className={styles.rowSep} aria-hidden="true">
|
|
344
|
+
·
|
|
345
|
+
</span>
|
|
346
|
+
<span className={styles.rowPreview}>{preview}</span>
|
|
347
|
+
</span>
|
|
348
|
+
|
|
349
|
+
<span className={styles.rowTime}>
|
|
350
|
+
{unreadCount > 0 && (
|
|
351
|
+
<span
|
|
352
|
+
className={styles.unreadDot}
|
|
353
|
+
aria-label={`${unreadCount} unread`}
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
{relativeTime(stamp)}
|
|
357
|
+
</span>
|
|
358
|
+
</li>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div className={styles.quickSms}>
|
|
364
|
+
<button
|
|
365
|
+
ref={triggerRef}
|
|
366
|
+
type="button"
|
|
367
|
+
className={styles.trigger}
|
|
368
|
+
onClick={() => (open ? close() : setOpen(true))}
|
|
369
|
+
data-tooltip-id="system-tooltip"
|
|
370
|
+
data-tooltip-place="bottom"
|
|
371
|
+
data-tooltip-content={label}
|
|
372
|
+
aria-label={unread > 0 ? `${label} (${unread} unread)` : label}
|
|
373
|
+
aria-expanded={open}
|
|
374
|
+
aria-haspopup="dialog"
|
|
375
|
+
>
|
|
376
|
+
<MessageSquare size={20} strokeWidth={2} />
|
|
377
|
+
{count !== '' && (
|
|
378
|
+
<span className={styles.badge} aria-hidden="true">
|
|
379
|
+
{count}
|
|
380
|
+
</span>
|
|
381
|
+
)}
|
|
382
|
+
</button>
|
|
383
|
+
|
|
384
|
+
{open && (
|
|
385
|
+
<div
|
|
386
|
+
className={styles.popoverWrapper}
|
|
387
|
+
ref={wrapperRef}
|
|
388
|
+
/* Tells the navigation's action pill to stop clipping while
|
|
389
|
+
this is open. See Navigation.module.scss `.hactions > ul`,
|
|
390
|
+
and Notification and the vault, which do the same. */
|
|
391
|
+
data-nav-overlay="open"
|
|
392
|
+
role="dialog"
|
|
393
|
+
aria-label={label}
|
|
394
|
+
>
|
|
395
|
+
<div className={styles.popover}>
|
|
396
|
+
<div className={styles.popoverHead}>
|
|
397
|
+
<span className={styles.popoverTitle}>
|
|
398
|
+
<MessageSquare size={15} strokeWidth={2.2} aria-hidden="true" />
|
|
399
|
+
{label}
|
|
400
|
+
</span>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
className={styles.primaryButton}
|
|
404
|
+
onClick={() => openThread(null)}
|
|
405
|
+
>
|
|
406
|
+
Open messages
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{threads.length > 0 && (
|
|
411
|
+
<ul
|
|
412
|
+
className={styles.rows}
|
|
413
|
+
ref={listRef}
|
|
414
|
+
role="listbox"
|
|
415
|
+
aria-label="Recent conversations"
|
|
416
|
+
tabIndex={-1}
|
|
417
|
+
onKeyDown={onKeyDown}
|
|
418
|
+
>
|
|
419
|
+
{threads.map(renderRow)}
|
|
420
|
+
</ul>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{threads.length === 0 && (
|
|
424
|
+
<div className={styles.empty}>
|
|
425
|
+
{loading || !loaded ? (
|
|
426
|
+
<p>
|
|
427
|
+
<Loader2
|
|
428
|
+
size={16}
|
|
429
|
+
className={styles.spin}
|
|
430
|
+
aria-label="Loading conversations"
|
|
431
|
+
/>
|
|
432
|
+
</p>
|
|
433
|
+
) : (
|
|
434
|
+
<p>
|
|
435
|
+
{status && status.lines_count === 0
|
|
436
|
+
? 'No messaging lines are set up yet.'
|
|
437
|
+
: 'No conversations yet.'}
|
|
438
|
+
</p>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
|
|
443
|
+
{note && (
|
|
444
|
+
<p className={styles.statusNote}>
|
|
445
|
+
{status?.transport === 'log' ? (
|
|
446
|
+
<TriangleAlert size={14} strokeWidth={2} aria-hidden="true" />
|
|
447
|
+
) : (
|
|
448
|
+
<Unplug size={14} strokeWidth={2} aria-hidden="true" />
|
|
449
|
+
)}
|
|
450
|
+
{note}
|
|
451
|
+
</p>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
<div className={styles.footer}>
|
|
455
|
+
<span className={styles.footerHint}>
|
|
456
|
+
↑↓ move · ⏎ open · Esc close
|
|
457
|
+
</span>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const SmsInboxBadge = withVaultNavigation(SmsInboxBadgeInner);
|
|
467
|
+
|
|
468
|
+
export default SmsInboxBadge;
|