@byscott-io/where-is-waldo 0.1.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/LICENSE +21 -0
- package/README.md +466 -0
- package/dist/index.esm.js +895 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +919 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/src/cable/index.js +120 -0
- package/src/context/PresenceContext.jsx +71 -0
- package/src/core/presenceReporter.js +163 -0
- package/src/core/rosterStore.js +117 -0
- package/src/hooks/usePresence.js +122 -0
- package/src/hooks/useRoster.js +138 -0
- package/src/hooks/useWaldoEvent.js +63 -0
- package/src/index.js +41 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import { createConsumer } from '@rails/actioncable';
|
|
2
|
+
import React, { useState, useRef, useEffect, useCallback, createContext, useMemo, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
function _arrayLikeToArray(r, a) {
|
|
5
|
+
(null == a || a > r.length) && (a = r.length);
|
|
6
|
+
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
7
|
+
return n;
|
|
8
|
+
}
|
|
9
|
+
function _arrayWithHoles(r) {
|
|
10
|
+
if (Array.isArray(r)) return r;
|
|
11
|
+
}
|
|
12
|
+
function _defineProperty(e, r, t) {
|
|
13
|
+
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
14
|
+
value: t,
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true
|
|
18
|
+
}) : e[r] = t, e;
|
|
19
|
+
}
|
|
20
|
+
function _iterableToArrayLimit(r, l) {
|
|
21
|
+
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
22
|
+
if (null != t) {
|
|
23
|
+
var e,
|
|
24
|
+
n,
|
|
25
|
+
i,
|
|
26
|
+
u,
|
|
27
|
+
a = [],
|
|
28
|
+
f = true,
|
|
29
|
+
o = false;
|
|
30
|
+
try {
|
|
31
|
+
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
|
|
32
|
+
} catch (r) {
|
|
33
|
+
o = true, n = r;
|
|
34
|
+
} finally {
|
|
35
|
+
try {
|
|
36
|
+
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
|
|
37
|
+
} finally {
|
|
38
|
+
if (o) throw n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return a;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function _nonIterableRest() {
|
|
45
|
+
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
46
|
+
}
|
|
47
|
+
function ownKeys(e, r) {
|
|
48
|
+
var t = Object.keys(e);
|
|
49
|
+
if (Object.getOwnPropertySymbols) {
|
|
50
|
+
var o = Object.getOwnPropertySymbols(e);
|
|
51
|
+
r && (o = o.filter(function (r) {
|
|
52
|
+
return Object.getOwnPropertyDescriptor(e, r).enumerable;
|
|
53
|
+
})), t.push.apply(t, o);
|
|
54
|
+
}
|
|
55
|
+
return t;
|
|
56
|
+
}
|
|
57
|
+
function _objectSpread2(e) {
|
|
58
|
+
for (var r = 1; r < arguments.length; r++) {
|
|
59
|
+
var t = null != arguments[r] ? arguments[r] : {};
|
|
60
|
+
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
|
|
61
|
+
_defineProperty(e, r, t[r]);
|
|
62
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
|
|
63
|
+
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return e;
|
|
67
|
+
}
|
|
68
|
+
function _slicedToArray(r, e) {
|
|
69
|
+
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
|
|
70
|
+
}
|
|
71
|
+
function _toPrimitive(t, r) {
|
|
72
|
+
if ("object" != typeof t || !t) return t;
|
|
73
|
+
var e = t[Symbol.toPrimitive];
|
|
74
|
+
if (void 0 !== e) {
|
|
75
|
+
var i = e.call(t, r);
|
|
76
|
+
if ("object" != typeof i) return i;
|
|
77
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
78
|
+
}
|
|
79
|
+
return ("string" === r ? String : Number)(t);
|
|
80
|
+
}
|
|
81
|
+
function _toPropertyKey(t) {
|
|
82
|
+
var i = _toPrimitive(t, "string");
|
|
83
|
+
return "symbol" == typeof i ? i : i + "";
|
|
84
|
+
}
|
|
85
|
+
function _unsupportedIterableToArray(r, a) {
|
|
86
|
+
if (r) {
|
|
87
|
+
if ("string" == typeof r) return _arrayLikeToArray(r, a);
|
|
88
|
+
var t = {}.toString.call(r).slice(8, -1);
|
|
89
|
+
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var consumer = null;
|
|
94
|
+
var config = {
|
|
95
|
+
url: '/cable',
|
|
96
|
+
getToken: null
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Subscribe-by-event-type registry: message type -> Set of listener fns.
|
|
100
|
+
// Components subscribe to a raw event type (see subscribeToEvent /
|
|
101
|
+
// useWaldoEvent) and decide for themselves whether they care about a given
|
|
102
|
+
// payload. handleMessage dispatches every incoming message to the listeners
|
|
103
|
+
// registered for its type.
|
|
104
|
+
var listeners = new Map();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to a raw cable event type. Returns an unsubscribe function.
|
|
108
|
+
* Multiple subscribers per type are supported; each receives (data, message).
|
|
109
|
+
* @param {string} type - The message type to listen for
|
|
110
|
+
* @param {Function} callback - Called with (data, message) on each matching event
|
|
111
|
+
* @returns {Function} unsubscribe
|
|
112
|
+
*/
|
|
113
|
+
function subscribeToEvent(type, callback) {
|
|
114
|
+
if (!type || typeof callback !== 'function') return function () {};
|
|
115
|
+
var set = listeners.get(type);
|
|
116
|
+
if (!set) {
|
|
117
|
+
set = new Set();
|
|
118
|
+
listeners.set(type, set);
|
|
119
|
+
}
|
|
120
|
+
set.add(callback);
|
|
121
|
+
return function unsubscribe() {
|
|
122
|
+
var current = listeners.get(type);
|
|
123
|
+
if (!current) return;
|
|
124
|
+
current["delete"](callback);
|
|
125
|
+
if (current.size === 0) listeners["delete"](type);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Configure the ActionCable connection
|
|
131
|
+
* @param {Object} options
|
|
132
|
+
* @param {string} options.url - WebSocket URL (default: '/cable')
|
|
133
|
+
* @param {Function} options.getToken - Function that returns auth token
|
|
134
|
+
*/
|
|
135
|
+
function configureCable() {
|
|
136
|
+
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
137
|
+
config = _objectSpread2(_objectSpread2({}, config), options);
|
|
138
|
+
// Reset consumer when config changes
|
|
139
|
+
if (consumer) {
|
|
140
|
+
consumer.disconnect();
|
|
141
|
+
consumer = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get or create the ActionCable consumer
|
|
147
|
+
* @returns {Consumer}
|
|
148
|
+
*/
|
|
149
|
+
function getConsumer() {
|
|
150
|
+
if (!consumer) {
|
|
151
|
+
var url = config.url;
|
|
152
|
+
if (config.getToken) {
|
|
153
|
+
var token = config.getToken();
|
|
154
|
+
if (token) {
|
|
155
|
+
var separator = url.includes('?') ? '&' : '?';
|
|
156
|
+
url = "".concat(url).concat(separator, "token=").concat(encodeURIComponent(token));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
consumer = createConsumer(url);
|
|
160
|
+
}
|
|
161
|
+
return consumer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Disconnect and reset the consumer
|
|
166
|
+
*/
|
|
167
|
+
function disconnectCable() {
|
|
168
|
+
if (consumer) {
|
|
169
|
+
consumer.disconnect();
|
|
170
|
+
consumer = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get current cable configuration
|
|
176
|
+
* @returns {Object}
|
|
177
|
+
*/
|
|
178
|
+
function getCableConfig() {
|
|
179
|
+
return _objectSpread2({}, config);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle an incoming message by dispatching to every listener subscribed to
|
|
184
|
+
* its type.
|
|
185
|
+
* @param {Object} message - The message object with type and data
|
|
186
|
+
* @returns {boolean} - Whether at least one listener was invoked
|
|
187
|
+
*/
|
|
188
|
+
function handleMessage(message) {
|
|
189
|
+
var type = message.type,
|
|
190
|
+
data = message.data;
|
|
191
|
+
var set = listeners.get(type);
|
|
192
|
+
if (!set || set.size === 0) return false;
|
|
193
|
+
|
|
194
|
+
// Iterate a snapshot so a listener that unsubscribes during dispatch is safe.
|
|
195
|
+
Array.from(set).forEach(function (cb) {
|
|
196
|
+
try {
|
|
197
|
+
cb(data, message);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
// One bad listener must not block the others.
|
|
200
|
+
// eslint-disable-next-line no-console
|
|
201
|
+
console.error("[where-is-waldo] listener for \"".concat(type, "\" threw:"), err);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
var DEFAULTS = {
|
|
208
|
+
channelName: 'WhereIsWaldo::PresenceChannel',
|
|
209
|
+
heartbeatInterval: 30000,
|
|
210
|
+
// ms
|
|
211
|
+
activityTimeout: 30000,
|
|
212
|
+
// ms of no activity -> subject_active=false
|
|
213
|
+
metadata: {},
|
|
214
|
+
debug: false
|
|
215
|
+
};
|
|
216
|
+
function createPresenceReporter() {
|
|
217
|
+
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
218
|
+
var config = _objectSpread2(_objectSpread2({}, DEFAULTS), options);
|
|
219
|
+
var now = config.now || function () {
|
|
220
|
+
return Date.now();
|
|
221
|
+
};
|
|
222
|
+
var resolveConsumer = config.getConsumer || getConsumer;
|
|
223
|
+
var resolveCableConfig = config.getCableConfig || getCableConfig;
|
|
224
|
+
var dispatch = config.handleMessage || handleMessage;
|
|
225
|
+
var log = function log() {
|
|
226
|
+
var _console;
|
|
227
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
228
|
+
args[_key] = arguments[_key];
|
|
229
|
+
}
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
if (config.debug) (_console = console).log.apply(_console, ['[PresenceReporter]'].concat(args));
|
|
232
|
+
};
|
|
233
|
+
var subscription = null;
|
|
234
|
+
var heartbeatTimer = null;
|
|
235
|
+
var activityTimer = null;
|
|
236
|
+
var lastActivityAt = 0;
|
|
237
|
+
|
|
238
|
+
// visible = tab/app is foreground & visible; active = recent user activity.
|
|
239
|
+
var state = {
|
|
240
|
+
connected: false,
|
|
241
|
+
visible: true,
|
|
242
|
+
active: true,
|
|
243
|
+
sessionId: null
|
|
244
|
+
};
|
|
245
|
+
var emitChange = function emitChange() {
|
|
246
|
+
if (config.onChange) config.onChange(_objectSpread2({}, state));
|
|
247
|
+
};
|
|
248
|
+
var buildPayload = function buildPayload() {
|
|
249
|
+
var _overrides$tab_visibl, _overrides$subject_ac;
|
|
250
|
+
var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
251
|
+
var payload = {
|
|
252
|
+
tab_visible: (_overrides$tab_visibl = overrides.tab_visible) !== null && _overrides$tab_visibl !== void 0 ? _overrides$tab_visibl : state.visible,
|
|
253
|
+
subject_active: (_overrides$subject_ac = overrides.subject_active) !== null && _overrides$subject_ac !== void 0 ? _overrides$subject_ac : state.active,
|
|
254
|
+
metadata: config.metadata || {}
|
|
255
|
+
};
|
|
256
|
+
if (lastActivityAt) payload.last_activity_at = lastActivityAt;
|
|
257
|
+
return payload;
|
|
258
|
+
};
|
|
259
|
+
var sendHeartbeat = function sendHeartbeat() {
|
|
260
|
+
var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
261
|
+
if (!subscription) return;
|
|
262
|
+
log('heartbeat', overrides);
|
|
263
|
+
subscription.perform('heartbeat', buildPayload(overrides));
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// (Re)arm the inactivity timer: after activityTimeout with no activity, flip
|
|
267
|
+
// to inactive and send an immediate heartbeat announcing it.
|
|
268
|
+
var scheduleInactivity = function scheduleInactivity() {
|
|
269
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
270
|
+
activityTimer = setTimeout(function () {
|
|
271
|
+
if (!state.active) return;
|
|
272
|
+
state.active = false;
|
|
273
|
+
emitChange();
|
|
274
|
+
sendHeartbeat({
|
|
275
|
+
subject_active: false
|
|
276
|
+
});
|
|
277
|
+
}, config.activityTimeout);
|
|
278
|
+
};
|
|
279
|
+
var reportActivity = function reportActivity() {
|
|
280
|
+
lastActivityAt = now();
|
|
281
|
+
if (!state.active) {
|
|
282
|
+
state.active = true;
|
|
283
|
+
emitChange();
|
|
284
|
+
sendHeartbeat({
|
|
285
|
+
subject_active: true
|
|
286
|
+
}); // immediate: inactive -> active
|
|
287
|
+
}
|
|
288
|
+
scheduleInactivity();
|
|
289
|
+
};
|
|
290
|
+
var setVisible = function setVisible(visible) {
|
|
291
|
+
if (visible === state.visible) return;
|
|
292
|
+
state.visible = visible;
|
|
293
|
+
emitChange();
|
|
294
|
+
sendHeartbeat(); // immediate: visibility change
|
|
295
|
+
if (visible) reportActivity();
|
|
296
|
+
};
|
|
297
|
+
var startHeartbeatLoop = function startHeartbeatLoop() {
|
|
298
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
299
|
+
heartbeatTimer = setInterval(function () {
|
|
300
|
+
return sendHeartbeat();
|
|
301
|
+
}, config.heartbeatInterval);
|
|
302
|
+
};
|
|
303
|
+
var start = function start() {
|
|
304
|
+
if (subscription) return subscription;
|
|
305
|
+
subscription = resolveConsumer().subscriptions.create({
|
|
306
|
+
channel: config.channelName,
|
|
307
|
+
metadata: config.metadata || {}
|
|
308
|
+
}, {
|
|
309
|
+
connected: function connected() {
|
|
310
|
+
state.connected = true;
|
|
311
|
+
var cable = resolveCableConfig();
|
|
312
|
+
if (cable && cable.sessionId) state.sessionId = cable.sessionId;
|
|
313
|
+
emitChange();
|
|
314
|
+
if (config.onConnected) config.onConnected();
|
|
315
|
+
startHeartbeatLoop();
|
|
316
|
+
},
|
|
317
|
+
disconnected: function disconnected() {
|
|
318
|
+
state.connected = false;
|
|
319
|
+
emitChange();
|
|
320
|
+
if (config.onDisconnected) config.onDisconnected();
|
|
321
|
+
},
|
|
322
|
+
received: function received(message) {
|
|
323
|
+
// Honor targeted (per-session) messages, then route to the event bus.
|
|
324
|
+
if (message && message._target_session && message._target_session !== state.sessionId) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
dispatch(message);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
scheduleInactivity();
|
|
331
|
+
return subscription;
|
|
332
|
+
};
|
|
333
|
+
var stop = function stop() {
|
|
334
|
+
if (heartbeatTimer) {
|
|
335
|
+
clearInterval(heartbeatTimer);
|
|
336
|
+
heartbeatTimer = null;
|
|
337
|
+
}
|
|
338
|
+
if (activityTimer) {
|
|
339
|
+
clearTimeout(activityTimer);
|
|
340
|
+
activityTimer = null;
|
|
341
|
+
}
|
|
342
|
+
if (subscription) {
|
|
343
|
+
subscription.unsubscribe();
|
|
344
|
+
subscription = null;
|
|
345
|
+
}
|
|
346
|
+
state.connected = false;
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
start: start,
|
|
350
|
+
stop: stop,
|
|
351
|
+
setVisible: setVisible,
|
|
352
|
+
reportActivity: reportActivity,
|
|
353
|
+
sendHeartbeat: sendHeartbeat,
|
|
354
|
+
getState: function getState() {
|
|
355
|
+
return _objectSpread2({}, state);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
var DEFAULT_OPTIONS = {
|
|
361
|
+
channelName: 'WhereIsWaldo::PresenceChannel',
|
|
362
|
+
heartbeatInterval: 30000,
|
|
363
|
+
activityTimeout: 30000,
|
|
364
|
+
trackActivity: true,
|
|
365
|
+
trackVisibility: true,
|
|
366
|
+
debug: false
|
|
367
|
+
};
|
|
368
|
+
var initialVisible = function initialVisible() {
|
|
369
|
+
return typeof document === 'undefined' ? true : document.visibilityState !== 'hidden';
|
|
370
|
+
};
|
|
371
|
+
var initialFocused = function initialFocused() {
|
|
372
|
+
return typeof document === 'undefined' ? true : document.hasFocus();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* usePresence - browser presence tracking. This is now a thin DOM-sensor
|
|
377
|
+
* wrapper around the framework-agnostic core (../core/presenceReporter): it
|
|
378
|
+
* binds `document`/`window` events to the reporter's `setVisible` /
|
|
379
|
+
* `reportActivity` inputs and mirrors the reporter's state into React state.
|
|
380
|
+
* The heartbeat cadence, activity/visibility state machine, and cable
|
|
381
|
+
* subscription all live in the core, which a React Native reporter reuses.
|
|
382
|
+
*
|
|
383
|
+
* @param {Object} options - see DEFAULT_OPTIONS
|
|
384
|
+
* @returns {{ connected, sessionId, tabVisible, windowFocused, subjectActive, sendHeartbeat }}
|
|
385
|
+
*/
|
|
386
|
+
function usePresence() {
|
|
387
|
+
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
388
|
+
var config = _objectSpread2(_objectSpread2({}, DEFAULT_OPTIONS), options);
|
|
389
|
+
var _useState = useState(false),
|
|
390
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
391
|
+
connected = _useState2[0],
|
|
392
|
+
setConnected = _useState2[1];
|
|
393
|
+
var _useState3 = useState(null),
|
|
394
|
+
_useState4 = _slicedToArray(_useState3, 2),
|
|
395
|
+
sessionId = _useState4[0],
|
|
396
|
+
setSessionId = _useState4[1];
|
|
397
|
+
var _useState5 = useState(true),
|
|
398
|
+
_useState6 = _slicedToArray(_useState5, 2),
|
|
399
|
+
subjectActive = _useState6[0],
|
|
400
|
+
setSubjectActive = _useState6[1];
|
|
401
|
+
var _useState7 = useState(initialVisible),
|
|
402
|
+
_useState8 = _slicedToArray(_useState7, 2),
|
|
403
|
+
tabVisible = _useState8[0],
|
|
404
|
+
setTabVisible = _useState8[1];
|
|
405
|
+
var _useState9 = useState(initialFocused),
|
|
406
|
+
_useState0 = _slicedToArray(_useState9, 2),
|
|
407
|
+
windowFocused = _useState0[0],
|
|
408
|
+
setWindowFocused = _useState0[1];
|
|
409
|
+
var reporterRef = useRef(null);
|
|
410
|
+
// Latest sensor readings, read by the DOM handlers to compute foreground.
|
|
411
|
+
var sensorRef = useRef({
|
|
412
|
+
tab: initialVisible(),
|
|
413
|
+
focus: initialFocused()
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Create + start the reporter once (re-created only if the channel changes).
|
|
417
|
+
useEffect(function () {
|
|
418
|
+
var reporter = createPresenceReporter({
|
|
419
|
+
channelName: config.channelName,
|
|
420
|
+
heartbeatInterval: config.heartbeatInterval,
|
|
421
|
+
activityTimeout: config.activityTimeout,
|
|
422
|
+
metadata: config.metadata,
|
|
423
|
+
debug: config.debug,
|
|
424
|
+
onConnected: config.onConnected,
|
|
425
|
+
onDisconnected: config.onDisconnected,
|
|
426
|
+
onChange: function onChange(state) {
|
|
427
|
+
setConnected(state.connected);
|
|
428
|
+
setSubjectActive(state.active);
|
|
429
|
+
if (state.sessionId) setSessionId(state.sessionId);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
reporterRef.current = reporter;
|
|
433
|
+
reporter.start();
|
|
434
|
+
// Seed foreground state from the current sensors.
|
|
435
|
+
reporter.setVisible(sensorRef.current.tab && sensorRef.current.focus);
|
|
436
|
+
return function () {
|
|
437
|
+
reporter.stop();
|
|
438
|
+
reporterRef.current = null;
|
|
439
|
+
};
|
|
440
|
+
}, [config.channelName]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
441
|
+
|
|
442
|
+
// Activity sensors -> reporter.reportActivity()
|
|
443
|
+
useEffect(function () {
|
|
444
|
+
if (!config.trackActivity) return undefined;
|
|
445
|
+
var onActivity = function onActivity() {
|
|
446
|
+
var _reporterRef$current;
|
|
447
|
+
return (_reporterRef$current = reporterRef.current) === null || _reporterRef$current === void 0 ? void 0 : _reporterRef$current.reportActivity();
|
|
448
|
+
};
|
|
449
|
+
var events = ['mousemove', 'keydown', 'scroll', 'touchstart', 'click'];
|
|
450
|
+
events.forEach(function (event) {
|
|
451
|
+
return window.addEventListener(event, onActivity, {
|
|
452
|
+
passive: true
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
return function () {
|
|
456
|
+
return events.forEach(function (event) {
|
|
457
|
+
return window.removeEventListener(event, onActivity);
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
}, [config.trackActivity]);
|
|
461
|
+
|
|
462
|
+
// Visibility + focus sensors -> reporter.setVisible(foreground)
|
|
463
|
+
useEffect(function () {
|
|
464
|
+
if (!config.trackVisibility) return undefined;
|
|
465
|
+
var pushForeground = function pushForeground() {
|
|
466
|
+
var _reporterRef$current2;
|
|
467
|
+
(_reporterRef$current2 = reporterRef.current) === null || _reporterRef$current2 === void 0 || _reporterRef$current2.setVisible(sensorRef.current.tab && sensorRef.current.focus);
|
|
468
|
+
};
|
|
469
|
+
var onVisibility = function onVisibility() {
|
|
470
|
+
var visible = document.visibilityState !== 'hidden';
|
|
471
|
+
sensorRef.current.tab = visible;
|
|
472
|
+
setTabVisible(visible);
|
|
473
|
+
pushForeground();
|
|
474
|
+
};
|
|
475
|
+
var onFocus = function onFocus() {
|
|
476
|
+
sensorRef.current.focus = true;
|
|
477
|
+
setWindowFocused(true);
|
|
478
|
+
pushForeground();
|
|
479
|
+
};
|
|
480
|
+
var onBlur = function onBlur() {
|
|
481
|
+
sensorRef.current.focus = false;
|
|
482
|
+
setWindowFocused(false);
|
|
483
|
+
pushForeground();
|
|
484
|
+
};
|
|
485
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
486
|
+
window.addEventListener('focus', onFocus);
|
|
487
|
+
window.addEventListener('blur', onBlur);
|
|
488
|
+
return function () {
|
|
489
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
490
|
+
window.removeEventListener('focus', onFocus);
|
|
491
|
+
window.removeEventListener('blur', onBlur);
|
|
492
|
+
};
|
|
493
|
+
}, [config.trackVisibility]);
|
|
494
|
+
var sendHeartbeat = useCallback(function (overrides) {
|
|
495
|
+
var _reporterRef$current3;
|
|
496
|
+
(_reporterRef$current3 = reporterRef.current) === null || _reporterRef$current3 === void 0 || _reporterRef$current3.sendHeartbeat(overrides);
|
|
497
|
+
}, []);
|
|
498
|
+
return {
|
|
499
|
+
connected: connected,
|
|
500
|
+
sessionId: sessionId,
|
|
501
|
+
tabVisible: tabVisible,
|
|
502
|
+
windowFocused: windowFocused,
|
|
503
|
+
subjectActive: subjectActive,
|
|
504
|
+
sendHeartbeat: sendHeartbeat
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
var PresenceContext = /*#__PURE__*/createContext(null);
|
|
509
|
+
var DEFAULT_CONFIG = {
|
|
510
|
+
channelName: 'WhereIsWaldo::PresenceChannel',
|
|
511
|
+
heartbeatInterval: 30000,
|
|
512
|
+
activityTimeout: 30000,
|
|
513
|
+
trackActivity: true,
|
|
514
|
+
trackVisibility: true,
|
|
515
|
+
debug: false
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get merged config from defaults, cable config, and overrides
|
|
520
|
+
*/
|
|
521
|
+
function getMergedConfig(configOverrides) {
|
|
522
|
+
var cableConfig = getCableConfig();
|
|
523
|
+
var presenceConfig = cableConfig.presence || {};
|
|
524
|
+
return _objectSpread2(_objectSpread2(_objectSpread2({}, DEFAULT_CONFIG), presenceConfig), configOverrides);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* PresenceProvider - Provides real-time presence tracking and message handling
|
|
529
|
+
*
|
|
530
|
+
* @param {Object} props
|
|
531
|
+
* @param {React.ReactNode} props.children
|
|
532
|
+
* @param {Object} props.metadata - Optional metadata to attach to presence
|
|
533
|
+
* @param {Object} props.config - Optional configuration overrides
|
|
534
|
+
* @param {Function} props.onConnected - Callback when connected
|
|
535
|
+
* @param {Function} props.onDisconnected - Callback when disconnected
|
|
536
|
+
*/
|
|
537
|
+
function PresenceProvider(_ref) {
|
|
538
|
+
var children = _ref.children,
|
|
539
|
+
_ref$metadata = _ref.metadata,
|
|
540
|
+
metadata = _ref$metadata === void 0 ? {} : _ref$metadata,
|
|
541
|
+
_ref$config = _ref.config,
|
|
542
|
+
configOverrides = _ref$config === void 0 ? {} : _ref$config,
|
|
543
|
+
onConnected = _ref.onConnected,
|
|
544
|
+
onDisconnected = _ref.onDisconnected;
|
|
545
|
+
var config = useMemo(function () {
|
|
546
|
+
return getMergedConfig(configOverrides);
|
|
547
|
+
}, [configOverrides]);
|
|
548
|
+
|
|
549
|
+
// Use the shared usePresence hook for all presence tracking
|
|
550
|
+
var presence = usePresence(_objectSpread2(_objectSpread2({}, config), {}, {
|
|
551
|
+
metadata: metadata,
|
|
552
|
+
onConnected: onConnected,
|
|
553
|
+
onDisconnected: onDisconnected
|
|
554
|
+
}));
|
|
555
|
+
return /*#__PURE__*/React.createElement(PresenceContext.Provider, {
|
|
556
|
+
value: presence
|
|
557
|
+
}, children);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Hook to access presence context
|
|
562
|
+
* @returns {Object} Presence context value
|
|
563
|
+
*/
|
|
564
|
+
function usePresenceContext() {
|
|
565
|
+
var context = useContext(PresenceContext);
|
|
566
|
+
if (!context) {
|
|
567
|
+
throw new Error('usePresenceContext must be used within a PresenceProvider');
|
|
568
|
+
}
|
|
569
|
+
return context;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* useWaldoEvent - subscribe a component to one or more raw real-time event types.
|
|
574
|
+
*
|
|
575
|
+
* The component subscribes to a message `type` (or an array of types) and
|
|
576
|
+
* receives the payload on every matching cable event; it decides for itself
|
|
577
|
+
* whether it cares about a given record (e.g. `if (data.project_id === myId)
|
|
578
|
+
* ...`). The subscription is automatically torn down when the component
|
|
579
|
+
* unmounts ("out of view"), exactly like a useEffect cleanup.
|
|
580
|
+
*
|
|
581
|
+
* The handler is held in a ref so it always sees the latest props/state without
|
|
582
|
+
* re-subscribing on every render — callers do NOT need to memoize it. The
|
|
583
|
+
* subscription is re-created only when the type(s) / `enabled` change.
|
|
584
|
+
*
|
|
585
|
+
* This is a pure subscription primitive — it has no timing/debounce concern.
|
|
586
|
+
* To coalesce a burst of events, compose with a debounce at the call site, e.g.
|
|
587
|
+
* corebyscott's `useDebouncedCallback`:
|
|
588
|
+
*
|
|
589
|
+
* const refetch = useDebouncedCallback(loadAll, 250);
|
|
590
|
+
* useWaldoEvent(['issue_update', 'project_update'], refetch);
|
|
591
|
+
*
|
|
592
|
+
* @param {string|string[]} type - Cable message type(s), e.g. 'issue_update'
|
|
593
|
+
* @param {Function} handler - Called with (data, message) on each event
|
|
594
|
+
* @param {Object} [options]
|
|
595
|
+
* @param {boolean} [options.enabled=true] - When false, no subscription is made
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* useWaldoEvent('issue_update', (data) => {
|
|
599
|
+
* if (data.id === issueId) setIssue((prev) => ({ ...prev, ...data }));
|
|
600
|
+
* });
|
|
601
|
+
*/
|
|
602
|
+
function useWaldoEvent(type, handler) {
|
|
603
|
+
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
604
|
+
var _options$enabled = options.enabled,
|
|
605
|
+
enabled = _options$enabled === void 0 ? true : _options$enabled;
|
|
606
|
+
var handlerRef = useRef(handler);
|
|
607
|
+
|
|
608
|
+
// Keep the ref pointing at the latest handler every render.
|
|
609
|
+
useEffect(function () {
|
|
610
|
+
handlerRef.current = handler;
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Stable, order-insensitive key so an inline array literal doesn't
|
|
614
|
+
// re-subscribe on every render.
|
|
615
|
+
var types = Array.isArray(type) ? type : [type];
|
|
616
|
+
var typesKey = types.filter(Boolean).join(' ');
|
|
617
|
+
useEffect(function () {
|
|
618
|
+
if (!enabled || !typesKey) return undefined;
|
|
619
|
+
var typeList = typesKey.split(' ');
|
|
620
|
+
var onEvent = function onEvent(data, message) {
|
|
621
|
+
if (handlerRef.current) handlerRef.current(data, message);
|
|
622
|
+
};
|
|
623
|
+
var unsubscribes = typeList.map(function (t) {
|
|
624
|
+
return subscribeToEvent(t, onEvent);
|
|
625
|
+
});
|
|
626
|
+
return function () {
|
|
627
|
+
unsubscribes.forEach(function (unsub) {
|
|
628
|
+
return unsub();
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
}, [typesKey, enabled]);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Framework-agnostic roster store: pure JS, no DOM and no React Native APIs, so
|
|
635
|
+
// it is safe to import from the web package AND the mobile (React Native)
|
|
636
|
+
// package. It owns the "materialize full state from a snapshot + transition
|
|
637
|
+
// deltas" logic and the derivations over it (per-device roll-up, sorting,
|
|
638
|
+
// labels, colors) — everything except the sensors and the pixels, which are
|
|
639
|
+
// necessarily platform-specific.
|
|
640
|
+
|
|
641
|
+
// Cable message types exchanged on the roster stream.
|
|
642
|
+
var ROSTER_SNAPSHOT = 'roster_snapshot';
|
|
643
|
+
var ROSTER_DELTA = 'roster_delta';
|
|
644
|
+
// Content-free "re-poll now" trigger (:nudge mode). Carries no data.
|
|
645
|
+
var ROSTER_NUDGE = 'roster_nudge';
|
|
646
|
+
|
|
647
|
+
// Activity ranking, most-present first. Shared by sorting and by the "highest
|
|
648
|
+
// across devices" roll-up so web and mobile agree.
|
|
649
|
+
var STATUS_RANK = {
|
|
650
|
+
active: 3,
|
|
651
|
+
idle: 2,
|
|
652
|
+
background: 1,
|
|
653
|
+
offline: 0
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Status -> presence dot color. Platform UIs may theme these, but sharing a
|
|
657
|
+
// default keeps web and mobile visually consistent out of the box.
|
|
658
|
+
var STATUS_COLORS = {
|
|
659
|
+
active: '#22c55e',
|
|
660
|
+
// visible + working
|
|
661
|
+
idle: '#f59e0b',
|
|
662
|
+
// visible but not actively using
|
|
663
|
+
background: '#9ca3af',
|
|
664
|
+
// only backgrounded/hidden sessions
|
|
665
|
+
offline: '#d1d5db' // no live sessions
|
|
666
|
+
};
|
|
667
|
+
function presenceColor(status) {
|
|
668
|
+
return STATUS_COLORS[status] || STATUS_COLORS.offline;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Apply an incoming cable message to a members Map (keyed by subject id),
|
|
673
|
+
* returning a NEW Map (immutable update) so callers can trigger re-renders by
|
|
674
|
+
* identity. Unknown message types return the same Map unchanged.
|
|
675
|
+
*
|
|
676
|
+
* - snapshot: replaces the whole roster.
|
|
677
|
+
* - delta: upserts one member. Offline members are KEPT (status "offline") so
|
|
678
|
+
* the roster stays the full team with live dots rather than vanishing rows.
|
|
679
|
+
*
|
|
680
|
+
* @param {Map<any, Object>} map - current members map
|
|
681
|
+
* @param {Object} message - { type, members? , member? }
|
|
682
|
+
* @returns {Map<any, Object>}
|
|
683
|
+
*/
|
|
684
|
+
function applyRosterMessage(map, message) {
|
|
685
|
+
if (!message || !message.type) return map;
|
|
686
|
+
if (message.type === ROSTER_SNAPSHOT) {
|
|
687
|
+
var next = new Map();
|
|
688
|
+
(message.members || []).forEach(function (m) {
|
|
689
|
+
return next.set(m.id, m);
|
|
690
|
+
});
|
|
691
|
+
return next;
|
|
692
|
+
}
|
|
693
|
+
if (message.type === ROSTER_DELTA && message.member) {
|
|
694
|
+
var _next = new Map(map);
|
|
695
|
+
if (message.member._removed) {
|
|
696
|
+
// Member left the viewer's visible scope (a visibility change) — drop it.
|
|
697
|
+
// Distinct from going offline, which arrives as status "offline".
|
|
698
|
+
_next["delete"](message.member.id);
|
|
699
|
+
} else {
|
|
700
|
+
_next.set(message.member.id, message.member);
|
|
701
|
+
}
|
|
702
|
+
return _next;
|
|
703
|
+
}
|
|
704
|
+
return map;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Modes that require the client to poll the server for deltas (vs. passively
|
|
709
|
+
* receiving pushed broadcasts). The server declares the mode in the snapshot.
|
|
710
|
+
*/
|
|
711
|
+
function isPollMode(mode) {
|
|
712
|
+
return mode === 'poll' || mode === 'nudge';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Members Map -> array, sorted most-present first then by display label.
|
|
717
|
+
*/
|
|
718
|
+
function sortedMembers(map) {
|
|
719
|
+
var list = Array.from(map.values());
|
|
720
|
+
list.sort(function (a, b) {
|
|
721
|
+
var _STATUS_RANK$b$status, _STATUS_RANK$a$status;
|
|
722
|
+
var rank = ((_STATUS_RANK$b$status = STATUS_RANK[b.status]) !== null && _STATUS_RANK$b$status !== void 0 ? _STATUS_RANK$b$status : 0) - ((_STATUS_RANK$a$status = STATUS_RANK[a.status]) !== null && _STATUS_RANK$a$status !== void 0 ? _STATUS_RANK$a$status : 0);
|
|
723
|
+
if (rank !== 0) return rank;
|
|
724
|
+
return String(memberLabel(a)).localeCompare(String(memberLabel(b)));
|
|
725
|
+
});
|
|
726
|
+
return list;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Only members with a live session (status !== "offline").
|
|
731
|
+
*/
|
|
732
|
+
function onlineMembers(members) {
|
|
733
|
+
return members.filter(function (m) {
|
|
734
|
+
return m.status && m.status !== 'offline';
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* That member's status on a specific device/platform, e.g.
|
|
740
|
+
* deviceStatus(member, 'mobile') // => "idle"
|
|
741
|
+
* Answers "is the user active on mobile?" vs the overall member.status.
|
|
742
|
+
*/
|
|
743
|
+
function deviceStatus(member, platform) {
|
|
744
|
+
return member && member.devices && member.devices[platform] || 'offline';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Best-effort display label from a member's subject data.
|
|
749
|
+
*/
|
|
750
|
+
function memberLabel(member) {
|
|
751
|
+
if (!member) return '';
|
|
752
|
+
return member.name || [member.first_name, member.last_name].filter(Boolean).join(' ') || member.email || member.username || "#".concat(member.id);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
var DEFAULT_CHANNEL = 'WhereIsWaldo::RosterChannel';
|
|
756
|
+
var DEFAULT_POLL_INTERVAL = 15; // seconds; overridden by the server's snapshot
|
|
757
|
+
var DEFAULT_NUDGE_JITTER = 0.5; // seconds; overridden by the server's snapshot
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* usePresenceRoster - live "who's around" roster as data (bring your own UI).
|
|
761
|
+
*
|
|
762
|
+
* The server picks the delivery mode per account and declares it in the initial
|
|
763
|
+
* snapshot. This hook adapts transparently — no client mode config:
|
|
764
|
+
* - broadcast/fanout : passively receive pushed deltas.
|
|
765
|
+
* - pull/nudge : poll the server (`perform('poll')`) on the interval the
|
|
766
|
+
* server specifies; the server replies with a filtered
|
|
767
|
+
* snapshot (resync) or diff.
|
|
768
|
+
* Either way the shared reducer (../core/rosterStore) keeps `members` as the
|
|
769
|
+
* full, live set (snapshot seeded, deltas patched, removals dropped).
|
|
770
|
+
*
|
|
771
|
+
* Each member: { id, status, devices: { web, mobile, ... }, ...subjectData }.
|
|
772
|
+
*
|
|
773
|
+
* @param {Object} [options]
|
|
774
|
+
* @param {string} [options.channelName] - override the roster channel name
|
|
775
|
+
* @param {boolean} [options.enabled=true] - when false, no subscription is made
|
|
776
|
+
* @param {Function} [options.filter] - (member) => boolean. Client-side filter
|
|
777
|
+
* applied to the returned members. COSMETIC ONLY — intended for `:broadcast`
|
|
778
|
+
* mode, where the full account roster reaches the client and you want to hide
|
|
779
|
+
* some rows in the UI. It is NOT an access-control boundary (the unfiltered
|
|
780
|
+
* data is already on the client). For enforced visibility use a server-side
|
|
781
|
+
* mode (`:poll`/`:nudge`/`:fanout`).
|
|
782
|
+
* @returns {{ members: Array, online: Array, onlineCount: number,
|
|
783
|
+
* byId: Object, connected: boolean, mode: string|null }}
|
|
784
|
+
*/
|
|
785
|
+
function usePresenceRoster() {
|
|
786
|
+
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
787
|
+
var _options$channelName = options.channelName,
|
|
788
|
+
channelName = _options$channelName === void 0 ? DEFAULT_CHANNEL : _options$channelName,
|
|
789
|
+
_options$enabled = options.enabled,
|
|
790
|
+
enabled = _options$enabled === void 0 ? true : _options$enabled,
|
|
791
|
+
filter = options.filter;
|
|
792
|
+
var _useState = useState([]),
|
|
793
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
794
|
+
members = _useState2[0],
|
|
795
|
+
setMembers = _useState2[1];
|
|
796
|
+
var _useState3 = useState(false),
|
|
797
|
+
_useState4 = _slicedToArray(_useState3, 2),
|
|
798
|
+
connected = _useState4[0],
|
|
799
|
+
setConnected = _useState4[1];
|
|
800
|
+
var _useState5 = useState(null),
|
|
801
|
+
_useState6 = _slicedToArray(_useState5, 2),
|
|
802
|
+
mode = _useState6[0],
|
|
803
|
+
setMode = _useState6[1];
|
|
804
|
+
var mapRef = useRef(new Map());
|
|
805
|
+
var pollTimerRef = useRef(null);
|
|
806
|
+
var nudgeTimerRef = useRef(null);
|
|
807
|
+
var nudgeJitterRef = useRef(DEFAULT_NUDGE_JITTER);
|
|
808
|
+
var flush = useCallback(function () {
|
|
809
|
+
setMembers(sortedMembers(mapRef.current));
|
|
810
|
+
}, []);
|
|
811
|
+
useEffect(function () {
|
|
812
|
+
if (!enabled) return undefined;
|
|
813
|
+
var consumer = getConsumer();
|
|
814
|
+
var startPolling = function startPolling(subscription, intervalSeconds) {
|
|
815
|
+
if (pollTimerRef.current) return; // already polling
|
|
816
|
+
var ms = (intervalSeconds || DEFAULT_POLL_INTERVAL) * 1000;
|
|
817
|
+
// Small random offset so clients don't poll in lockstep (herd control).
|
|
818
|
+
var jitter = function jitter() {
|
|
819
|
+
return 0.85 + Math.random() * 0.3;
|
|
820
|
+
};
|
|
821
|
+
var _tick = function tick() {
|
|
822
|
+
subscription.perform('poll');
|
|
823
|
+
pollTimerRef.current = setTimeout(_tick, ms * jitter());
|
|
824
|
+
};
|
|
825
|
+
pollTimerRef.current = setTimeout(_tick, ms * jitter());
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// :nudge — on a content-free trigger, re-poll off-cycle after a random
|
|
829
|
+
// jitter (herd control), debounced to one pending re-poll at a time.
|
|
830
|
+
var scheduleNudgePoll = function scheduleNudgePoll(subscription) {
|
|
831
|
+
if (nudgeTimerRef.current) return;
|
|
832
|
+
var delay = Math.random() * nudgeJitterRef.current * 1000;
|
|
833
|
+
nudgeTimerRef.current = setTimeout(function () {
|
|
834
|
+
nudgeTimerRef.current = null;
|
|
835
|
+
subscription.perform('poll');
|
|
836
|
+
}, delay);
|
|
837
|
+
};
|
|
838
|
+
var subscription = consumer.subscriptions.create({
|
|
839
|
+
channel: channelName
|
|
840
|
+
}, {
|
|
841
|
+
connected: function connected() {
|
|
842
|
+
setConnected(true);
|
|
843
|
+
},
|
|
844
|
+
disconnected: function disconnected() {
|
|
845
|
+
setConnected(false);
|
|
846
|
+
},
|
|
847
|
+
received: function received(message) {
|
|
848
|
+
if (message && message.type === ROSTER_SNAPSHOT) {
|
|
849
|
+
setMode(message.mode || null);
|
|
850
|
+
if (message.nudge_jitter != null) nudgeJitterRef.current = message.nudge_jitter;
|
|
851
|
+
if (isPollMode(message.mode)) startPolling(subscription, message.poll_interval);
|
|
852
|
+
}
|
|
853
|
+
if (message && message.type === ROSTER_NUDGE) {
|
|
854
|
+
scheduleNudgePoll(subscription);
|
|
855
|
+
return; // content-free trigger; nothing to reduce
|
|
856
|
+
}
|
|
857
|
+
var next = applyRosterMessage(mapRef.current, message);
|
|
858
|
+
if (next !== mapRef.current) {
|
|
859
|
+
mapRef.current = next;
|
|
860
|
+
flush();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
return function () {
|
|
865
|
+
if (pollTimerRef.current) {
|
|
866
|
+
clearTimeout(pollTimerRef.current);
|
|
867
|
+
pollTimerRef.current = null;
|
|
868
|
+
}
|
|
869
|
+
if (nudgeTimerRef.current) {
|
|
870
|
+
clearTimeout(nudgeTimerRef.current);
|
|
871
|
+
nudgeTimerRef.current = null;
|
|
872
|
+
}
|
|
873
|
+
subscription.unsubscribe();
|
|
874
|
+
};
|
|
875
|
+
}, [channelName, enabled, flush]);
|
|
876
|
+
|
|
877
|
+
// Client-side (cosmetic) filter — see options.filter.
|
|
878
|
+
var visibleMembers = filter ? members.filter(filter) : members;
|
|
879
|
+
var online = onlineMembers(visibleMembers);
|
|
880
|
+
var byId = {};
|
|
881
|
+
visibleMembers.forEach(function (m) {
|
|
882
|
+
byId[m.id] = m;
|
|
883
|
+
});
|
|
884
|
+
return {
|
|
885
|
+
members: visibleMembers,
|
|
886
|
+
online: online,
|
|
887
|
+
onlineCount: online.length,
|
|
888
|
+
byId: byId,
|
|
889
|
+
connected: connected,
|
|
890
|
+
mode: mode
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export { PresenceContext, PresenceProvider, ROSTER_DELTA, ROSTER_SNAPSHOT, STATUS_COLORS, STATUS_RANK, applyRosterMessage, configureCable, createPresenceReporter, deviceStatus, disconnectCable, getCableConfig, getConsumer, handleMessage, memberLabel, onlineMembers, presenceColor, sortedMembers, subscribeToEvent, usePresence, usePresenceContext, usePresenceRoster, useWaldoEvent };
|
|
895
|
+
//# sourceMappingURL=index.esm.js.map
|