@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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bookkeeping behind "is this inbox actually live?".
|
|
3
|
+
*
|
|
4
|
+
* Split out of `useSmsLive` so it can be tested without React, and because the
|
|
5
|
+
* question is subtler than it looks. Calling `echo.private(name)` returns a
|
|
6
|
+
* channel object immediately — before `/broadcasting/auth` has been asked, let
|
|
7
|
+
* alone answered. A 403 or a 419 there, or a websocket that never connects at
|
|
8
|
+
* all, leaves the consumer holding a channel that will never deliver an event.
|
|
9
|
+
* If that counts as "live" the polling fallback switches itself off and the
|
|
10
|
+
* page stops refreshing entirely, which is precisely the failure we saw in the
|
|
11
|
+
* field: an open inbox and not one occupied channel on Pusher.
|
|
12
|
+
*
|
|
13
|
+
* So live means *confirmed*: at least one channel has reported
|
|
14
|
+
* `pusher:subscription_succeeded`, and the connection underneath it has not
|
|
15
|
+
* since gone away.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connection states in which nothing can arrive, whatever the channels think.
|
|
20
|
+
*
|
|
21
|
+
* pusher-js resubscribes by itself once it reconnects, so these are not
|
|
22
|
+
* terminal — they just mean "poll in the meantime".
|
|
23
|
+
*/
|
|
24
|
+
export const DEAD_CONNECTION_STATES = ['unavailable', 'failed', 'disconnected'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Live iff a channel has confirmed and the socket is not known to be down.
|
|
28
|
+
*
|
|
29
|
+
* @param {Set<string>|Array<string>|number|null} succeeded Confirmed channels.
|
|
30
|
+
* @param {string|null} [connectionState] Latest pusher connection state.
|
|
31
|
+
*
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export const deriveLiveState = (succeeded, connectionState = null) => {
|
|
35
|
+
let count = 0;
|
|
36
|
+
|
|
37
|
+
if (typeof succeeded === 'number') count = succeeded;
|
|
38
|
+
else if (succeeded instanceof Set) count = succeeded.size;
|
|
39
|
+
else if (Array.isArray(succeeded)) count = succeeded.length;
|
|
40
|
+
|
|
41
|
+
if (count <= 0) return false;
|
|
42
|
+
|
|
43
|
+
return !DEAD_CONNECTION_STATES.includes(connectionState);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Whether a connection state means "nothing will arrive right now". */
|
|
47
|
+
export const isDeadConnectionState = (state) => DEAD_CONNECTION_STATES.includes(state);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Watch one subscription for its authorisation result.
|
|
51
|
+
*
|
|
52
|
+
* Echo's `PusherPrivateChannel` exposes `.subscribed(cb)` and `.error(cb)`;
|
|
53
|
+
* older versions, and anything hand-rolled, only expose the raw pusher-js
|
|
54
|
+
* channel at `.subscription`, which we bind the same two events on directly.
|
|
55
|
+
* Either way the caller learns whether the channel is real.
|
|
56
|
+
*
|
|
57
|
+
* Everything is guarded: a channel object that is missing one of these must
|
|
58
|
+
* cost us the confirmation, not the other channels or the render.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} subscription Echo channel (or a stand-in in tests).
|
|
61
|
+
* @param {object} handlers
|
|
62
|
+
* @param {Function} [handlers.onSuccess] Called on subscription_succeeded.
|
|
63
|
+
* @param {Function} [handlers.onError] Called on subscription_error.
|
|
64
|
+
*
|
|
65
|
+
* @returns {Function} Unbinds whatever was bound. Always safe to call.
|
|
66
|
+
*/
|
|
67
|
+
export const subscriptionMonitor = (subscription, { onSuccess, onError } = {}) => {
|
|
68
|
+
if (!subscription) return () => {};
|
|
69
|
+
|
|
70
|
+
const success = () => {
|
|
71
|
+
try {
|
|
72
|
+
onSuccess?.();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// A consumer's setState must not break the pusher callback chain.
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const failure = (payload) => {
|
|
79
|
+
try {
|
|
80
|
+
onError?.(payload);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Same.
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Preferred path: Echo's own callbacks.
|
|
87
|
+
if (typeof subscription.subscribed === 'function') {
|
|
88
|
+
let bound = false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
subscription.subscribed(success);
|
|
92
|
+
bound = true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Fall through to the raw channel below.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (bound) {
|
|
98
|
+
try {
|
|
99
|
+
if (typeof subscription.error === 'function') subscription.error(failure);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// An unwatchable error channel is not worth failing over.
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Echo gives no way to unbind these individually; leaving the
|
|
105
|
+
// channel (which the hook does on cleanup) drops them with it.
|
|
106
|
+
return () => {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fallback: the raw pusher-js channel.
|
|
111
|
+
const raw = subscription.subscription;
|
|
112
|
+
|
|
113
|
+
if (!raw || typeof raw.bind !== 'function') return () => {};
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
raw.bind('pusher:subscription_succeeded', success);
|
|
117
|
+
raw.bind('pusher:subscription_error', failure);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return () => {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
try {
|
|
124
|
+
raw.unbind?.('pusher:subscription_succeeded', success);
|
|
125
|
+
raw.unbind?.('pusher:subscription_error', failure);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Already gone.
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export default deriveLiveState;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import CustomFetch from '../Fetch';
|
|
4
|
+
import { resolveSmsEndpoints } from './smsEndpoints';
|
|
5
|
+
import { deriveLiveState, isDeadConnectionState, subscriptionMonitor } from './smsLiveState';
|
|
6
|
+
|
|
7
|
+
/** The two events the backend broadcasts on a line's channel. */
|
|
8
|
+
export const EVENT_RECEIVED = '.sms.received';
|
|
9
|
+
export const EVENT_UPDATED = '.sms.updated';
|
|
10
|
+
|
|
11
|
+
/** The channel a line broadcasts on, when the consumer does not name one. */
|
|
12
|
+
export const defaultChannelFor = (lineId) => `sms-line.${lineId}`;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Live messaging: Pusher when it is wired up, polling when it is not.
|
|
16
|
+
*
|
|
17
|
+
* Both halves matter. The CRM has Echo, so the inbox updates the moment a
|
|
18
|
+
* reply lands. The fixture, a tab with a dead websocket, and any deployment
|
|
19
|
+
* where broadcasting is not configured have no Echo at all — and a messaging
|
|
20
|
+
* inbox that silently stops updating is worse than one that was never live,
|
|
21
|
+
* because nothing on screen says so. So the fallback is not an error state,
|
|
22
|
+
* it is a slower version of the same behaviour: `/unread` every thirty
|
|
23
|
+
* seconds, plus whatever the mounted surface wants to re-fetch alongside it.
|
|
24
|
+
*
|
|
25
|
+
* What "live" means here is deliberately strict. `instance.private(name)`
|
|
26
|
+
* hands back a channel object straight away, long before
|
|
27
|
+
* `/broadcasting/auth` has approved it — so treating that as live is how an
|
|
28
|
+
* inbox ends up subscribed to nothing, polling nothing, and showing a
|
|
29
|
+
* conversation that stopped updating an hour ago. Live therefore waits for
|
|
30
|
+
* `pusher:subscription_succeeded` on at least one channel, and drops again if
|
|
31
|
+
* the connection goes `unavailable`/`failed`/`disconnected`. A subscription
|
|
32
|
+
* whose auth fails must never be allowed to silence the poller.
|
|
33
|
+
*
|
|
34
|
+
* Even once live, a slow safety-net poll keeps running (`livePollInterval`,
|
|
35
|
+
* a minute by default) so a single dropped event costs a minute of staleness
|
|
36
|
+
* rather than the rest of the session. It skips the immediate first tick —
|
|
37
|
+
* the subscription is already delivering.
|
|
38
|
+
*
|
|
39
|
+
* The `echo` prop is an instance OR a factory, matching CallQueuePop: a
|
|
40
|
+
* factory is called inside the effect rather than at mount, so a user without
|
|
41
|
+
* the permission never opens a Pusher connection at all.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} options
|
|
44
|
+
* @param {object|Function|null} options.echo Echo instance, or `() => echo`.
|
|
45
|
+
* @param {Array} options.lines `[{id}]` — the lines to listen on.
|
|
46
|
+
* @param {Function} [options.channelFor] `(lineId) => 'sms-line.7.dev'`.
|
|
47
|
+
* @param {object} [options.endpoints] Partial endpoints override.
|
|
48
|
+
* @param {boolean} [options.enabled] Off entirely when false.
|
|
49
|
+
* @param {Function} [options.onReceived] `({thread, message}) => void`.
|
|
50
|
+
* @param {Function} [options.onUpdated] `({thread, message}) => void`.
|
|
51
|
+
* @param {Function} [options.onUnread] `({total, by_line, by_thread}) => void`.
|
|
52
|
+
* @param {Function} [options.onPoll] Ticks on every poll, live or not.
|
|
53
|
+
* @param {number} [options.pollInterval] Fallback poll; default 30s.
|
|
54
|
+
* @param {number} [options.livePollInterval] Safety net while live; default 60s, 0 off.
|
|
55
|
+
*
|
|
56
|
+
* @returns {{live: boolean, refreshUnread: Function, subscribed: number}}
|
|
57
|
+
*/
|
|
58
|
+
const useSmsLive = ({
|
|
59
|
+
echo = null,
|
|
60
|
+
lines = [],
|
|
61
|
+
channelFor = defaultChannelFor,
|
|
62
|
+
endpoints,
|
|
63
|
+
enabled = true,
|
|
64
|
+
onReceived,
|
|
65
|
+
onUpdated,
|
|
66
|
+
onUnread,
|
|
67
|
+
onPoll,
|
|
68
|
+
pollInterval = 30_000,
|
|
69
|
+
livePollInterval = 60_000,
|
|
70
|
+
} = {}) => {
|
|
71
|
+
const [live, setLive] = useState(false);
|
|
72
|
+
const [subscribed, setSubscribed] = useState(0);
|
|
73
|
+
|
|
74
|
+
// Everything below is read through a ref so that a consumer passing an
|
|
75
|
+
// inline arrow — `onReceived={(e) => ...}`, `echo={() => getEcho()}`,
|
|
76
|
+
// which is the normal way to write both — does not tear the subscription
|
|
77
|
+
// down and rebuild it on every render. Only the channel names do that.
|
|
78
|
+
const echoRef = useRef(echo);
|
|
79
|
+
echoRef.current = echo;
|
|
80
|
+
|
|
81
|
+
const channelForRef = useRef(channelFor);
|
|
82
|
+
channelForRef.current = channelFor;
|
|
83
|
+
|
|
84
|
+
const receivedRef = useRef(onReceived);
|
|
85
|
+
receivedRef.current = onReceived;
|
|
86
|
+
|
|
87
|
+
const updatedRef = useRef(onUpdated);
|
|
88
|
+
updatedRef.current = onUpdated;
|
|
89
|
+
|
|
90
|
+
const unreadRef = useRef(onUnread);
|
|
91
|
+
unreadRef.current = onUnread;
|
|
92
|
+
|
|
93
|
+
const pollRef = useRef(onPoll);
|
|
94
|
+
pollRef.current = onPoll;
|
|
95
|
+
|
|
96
|
+
const mountedRef = useRef(true);
|
|
97
|
+
|
|
98
|
+
// The channels that have actually been confirmed, and the last connection
|
|
99
|
+
// state we heard about — the two inputs `live` is derived from.
|
|
100
|
+
const succeededRef = useRef(new Set());
|
|
101
|
+
const connectionStateRef = useRef(null);
|
|
102
|
+
|
|
103
|
+
useEffect(
|
|
104
|
+
() => () => {
|
|
105
|
+
mountedRef.current = false;
|
|
106
|
+
},
|
|
107
|
+
[]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const routes = resolveSmsEndpoints(endpoints);
|
|
111
|
+
const unreadUrl = routes.unread;
|
|
112
|
+
|
|
113
|
+
/** Pull the unread counts once. Safe to call from anywhere. */
|
|
114
|
+
const refreshUnread = useCallback(() => {
|
|
115
|
+
CustomFetch(
|
|
116
|
+
unreadUrl,
|
|
117
|
+
'GET',
|
|
118
|
+
null,
|
|
119
|
+
(result) => {
|
|
120
|
+
if (!mountedRef.current) return;
|
|
121
|
+
|
|
122
|
+
unreadRef.current?.(result || {});
|
|
123
|
+
},
|
|
124
|
+
// Silent: a background count that fails must not toast at someone
|
|
125
|
+
// in the middle of typing.
|
|
126
|
+
() => {}
|
|
127
|
+
).catch(() => {});
|
|
128
|
+
}, [unreadUrl]);
|
|
129
|
+
|
|
130
|
+
/** Re-read the two inputs and publish the answer. */
|
|
131
|
+
const syncLive = useCallback(() => {
|
|
132
|
+
if (!mountedRef.current) return;
|
|
133
|
+
|
|
134
|
+
const count = succeededRef.current.size;
|
|
135
|
+
|
|
136
|
+
setSubscribed(count);
|
|
137
|
+
setLive(deriveLiveState(count, connectionStateRef.current));
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
// The channel names, as one stable string, so the effect below re-runs
|
|
141
|
+
// when the set of lines actually changes and not when the array identity
|
|
142
|
+
// does — `lines` is rebuilt by every fetch.
|
|
143
|
+
const channelKey = (Array.isArray(lines) ? lines : [])
|
|
144
|
+
.map((line) => (line && line.id !== undefined ? line.id : line))
|
|
145
|
+
.filter((id) => id !== undefined && id !== null)
|
|
146
|
+
.map((id) => channelForRef.current(id))
|
|
147
|
+
.join('|');
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
succeededRef.current = new Set();
|
|
151
|
+
connectionStateRef.current = null;
|
|
152
|
+
|
|
153
|
+
if (!enabled || channelKey === '' || typeof window === 'undefined') {
|
|
154
|
+
syncLive();
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const source = echoRef.current;
|
|
160
|
+
const instance = typeof source === 'function' ? source() : source;
|
|
161
|
+
|
|
162
|
+
if (!instance) {
|
|
163
|
+
syncLive();
|
|
164
|
+
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const names = channelKey.split('|');
|
|
169
|
+
const subscriptions = [];
|
|
170
|
+
const monitors = [];
|
|
171
|
+
|
|
172
|
+
names.forEach((name) => {
|
|
173
|
+
try {
|
|
174
|
+
// Until /broadcasting/auth knows the channel the subscription
|
|
175
|
+
// just fails auth — pusher-js logs a warning, nothing of ours
|
|
176
|
+
// throws — so a line the user cannot see is harmless here. It
|
|
177
|
+
// simply never joins the confirmed set below.
|
|
178
|
+
const subscription = instance.private(name);
|
|
179
|
+
|
|
180
|
+
subscription.listen(EVENT_RECEIVED, (event) => {
|
|
181
|
+
receivedRef.current?.(event || {});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
subscription.listen(EVENT_UPDATED, (event) => {
|
|
185
|
+
updatedRef.current?.(event || {});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
monitors.push(
|
|
189
|
+
subscriptionMonitor(subscription, {
|
|
190
|
+
onSuccess: () => {
|
|
191
|
+
succeededRef.current.add(name);
|
|
192
|
+
syncLive();
|
|
193
|
+
},
|
|
194
|
+
onError: () => {
|
|
195
|
+
succeededRef.current.delete(name);
|
|
196
|
+
syncLive();
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
subscriptions.push({ name, subscription });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// One bad channel must not cost the others their subscription.
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Not live yet — that waits for a subscribed callback. The poller
|
|
208
|
+
// covers the grace window in between, including its immediate tick.
|
|
209
|
+
syncLive();
|
|
210
|
+
|
|
211
|
+
// The socket underneath. When it goes, every channel goes with it,
|
|
212
|
+
// whatever the confirmed set still says; pusher-js resubscribes on
|
|
213
|
+
// its own once it comes back, so we clear the set and let the
|
|
214
|
+
// subscribed callbacks re-fill it.
|
|
215
|
+
let unbindState = () => {};
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const connection = instance.connector?.pusher?.connection;
|
|
219
|
+
|
|
220
|
+
if (connection && typeof connection.bind === 'function') {
|
|
221
|
+
const onStateChange = (states) => {
|
|
222
|
+
const current = states?.current ?? states;
|
|
223
|
+
|
|
224
|
+
connectionStateRef.current = current;
|
|
225
|
+
|
|
226
|
+
if (isDeadConnectionState(current)) succeededRef.current = new Set();
|
|
227
|
+
|
|
228
|
+
syncLive();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
connection.bind('state_change', onStateChange);
|
|
232
|
+
connectionStateRef.current = connection.state ?? null;
|
|
233
|
+
|
|
234
|
+
unbindState = () => {
|
|
235
|
+
try {
|
|
236
|
+
connection.unbind?.('state_change', onStateChange);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// Already gone.
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// No reachable connection object: channel confirmations alone
|
|
244
|
+
// decide, which is the same answer we gave before.
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
unbindState();
|
|
249
|
+
monitors.forEach((unbind) => {
|
|
250
|
+
try {
|
|
251
|
+
unbind();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// Already gone.
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
subscriptions.forEach(({ name, subscription }) => {
|
|
258
|
+
try {
|
|
259
|
+
subscription.stopListening(EVENT_RECEIVED);
|
|
260
|
+
subscription.stopListening(EVENT_UPDATED);
|
|
261
|
+
instance.leave(name);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Nothing to clean up.
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
succeededRef.current = new Set();
|
|
268
|
+
connectionStateRef.current = null;
|
|
269
|
+
};
|
|
270
|
+
}, [enabled, channelKey, syncLive]);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The fallback, and the safety net.
|
|
274
|
+
*
|
|
275
|
+
* With nothing confirmed live this is the only thing keeping the page
|
|
276
|
+
* fresh, so it starts immediately and runs at `pollInterval`. Once a
|
|
277
|
+
* channel confirms it drops back to `livePollInterval` — slow enough to
|
|
278
|
+
* be free, quick enough that a dropped event is invisible — and skips the
|
|
279
|
+
* immediate tick, because the subscription is already delivering.
|
|
280
|
+
*
|
|
281
|
+
* `onPoll` is what a mounted inbox hangs its thread-list refresh on, so
|
|
282
|
+
* the page makes one decision about how often to talk to the server
|
|
283
|
+
* rather than two.
|
|
284
|
+
*/
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (!enabled) return undefined;
|
|
287
|
+
|
|
288
|
+
const interval = live ? livePollInterval : pollInterval;
|
|
289
|
+
|
|
290
|
+
if (!(interval > 0)) return undefined;
|
|
291
|
+
|
|
292
|
+
const tick = () => {
|
|
293
|
+
refreshUnread();
|
|
294
|
+
pollRef.current?.();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (!live) tick();
|
|
298
|
+
|
|
299
|
+
const timer = setInterval(tick, interval);
|
|
300
|
+
|
|
301
|
+
return () => clearInterval(timer);
|
|
302
|
+
}, [enabled, live, pollInterval, livePollInterval, refreshUnread]);
|
|
303
|
+
|
|
304
|
+
return { live, refreshUnread, subscribed };
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export default useSmsLive;
|