@poolse/react 2.14.0 → 2.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/CHANGELOG.md +42 -0
- package/dist/index.cjs +69 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +69 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to `@poolse/react` are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[semver](https://semver.org).
|
|
6
6
|
|
|
7
|
+
## [2.15.0] — 2026-08-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Answering a call then hanging up left the other side in it.**
|
|
12
|
+
`accept()` clears `incoming`, and a callee never had an `outgoing`, so
|
|
13
|
+
`hangUp()` resolved no call id and returned before signalling. The
|
|
14
|
+
call id of the call in progress is now tracked in its own right.
|
|
15
|
+
- **A late `call:cancelled` tore down a live call.** The guard fell
|
|
16
|
+
through whenever `incoming` was null — exactly the state `accept()`
|
|
17
|
+
leaves behind — so a caller's ring timeout firing as the callee
|
|
18
|
+
answered dropped the callee straight out of the call.
|
|
19
|
+
- **Every call signal is now matched to the call it belongs to.**
|
|
20
|
+
`accepted`, `declined`, `busy`, `ended` and `cancelled` are ignored
|
|
21
|
+
unless they name the call this client is actually in. A signal about a
|
|
22
|
+
previous call could previously end the current one.
|
|
23
|
+
- **A room could be left connected with nothing holding it.** If a call
|
|
24
|
+
ended while its media was still connecting, the cleanup ran before
|
|
25
|
+
`join()` resolved, found nothing to release, and the connection
|
|
26
|
+
completed into a room no component held — audio still flowing, no UI,
|
|
27
|
+
and no way to hang up. Both `useCallRoom` and `useVoiceRoom` now
|
|
28
|
+
release a room that was superseded mid-connect.
|
|
29
|
+
- **`markBusy` could reject a call the user was being shown.** The
|
|
30
|
+
decision ran inside a `setPhase` updater; updaters must be pure, and
|
|
31
|
+
React may run them more than once.
|
|
32
|
+
|
|
33
|
+
## [2.14.1] — 2026-08-23
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`useCallRoom().refreshDevices`** — re-read the device list on
|
|
38
|
+
demand. Devices are already re-read on connect and on hardware
|
|
39
|
+
changes; this covers the case neither does, which is that a browser
|
|
40
|
+
only reveals device ids and labels once the page holds a matching
|
|
41
|
+
permission, and that moment is not subscribable. Call it when opening
|
|
42
|
+
a picker.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Device lists inherited the `MediaDeviceInfo` spread bug from
|
|
47
|
+
`@poolse/sdk`. See its 2.14.1 entry.
|
|
48
|
+
|
|
7
49
|
## [2.14.0] — 2026-08-23
|
|
8
50
|
|
|
9
51
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -1082,6 +1082,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1082
1082
|
const optsRef = react.useRef(opts);
|
|
1083
1083
|
optsRef.current = opts;
|
|
1084
1084
|
const joined = react.useRef(false);
|
|
1085
|
+
const held = react.useRef(null);
|
|
1085
1086
|
const room = react.useMemo(() => {
|
|
1086
1087
|
if (!conversationId) return null;
|
|
1087
1088
|
return poolse.call(conversationId, {
|
|
@@ -1121,6 +1122,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1121
1122
|
}
|
|
1122
1123
|
setStatus(room.getStatus());
|
|
1123
1124
|
setParticipants(room.getParticipants());
|
|
1125
|
+
held.current = room;
|
|
1124
1126
|
setAlone(false);
|
|
1125
1127
|
const offStatus = room.onStatus(setStatus);
|
|
1126
1128
|
const offRoster = room.onParticipants((next) => {
|
|
@@ -1136,6 +1138,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1136
1138
|
offAlone();
|
|
1137
1139
|
offDevices();
|
|
1138
1140
|
offError();
|
|
1141
|
+
held.current = null;
|
|
1139
1142
|
if (joined.current) {
|
|
1140
1143
|
joined.current = false;
|
|
1141
1144
|
room.leave();
|
|
@@ -1143,14 +1146,19 @@ function useCallRoom(conversationId, opts) {
|
|
|
1143
1146
|
};
|
|
1144
1147
|
}, [room, sync, refreshDevices]);
|
|
1145
1148
|
const join = react.useCallback(async () => {
|
|
1146
|
-
|
|
1149
|
+
const target = room;
|
|
1150
|
+
if (!target) return false;
|
|
1147
1151
|
setError(null);
|
|
1148
1152
|
try {
|
|
1149
|
-
const connected = await
|
|
1153
|
+
const connected = await target.join();
|
|
1154
|
+
if (held.current !== target) {
|
|
1155
|
+
if (connected) target.leave();
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1150
1158
|
joined.current = connected;
|
|
1151
1159
|
setSfuAvailable(connected);
|
|
1152
|
-
sync(
|
|
1153
|
-
if (connected) refreshDevices(
|
|
1160
|
+
sync(target);
|
|
1161
|
+
if (connected) refreshDevices(target);
|
|
1154
1162
|
return connected;
|
|
1155
1163
|
} catch (err) {
|
|
1156
1164
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1192,6 +1200,9 @@ function useCallRoom(conversationId, opts) {
|
|
|
1192
1200
|
await room.switchCamera();
|
|
1193
1201
|
setFacing(room.getFacingMode());
|
|
1194
1202
|
}, [room]);
|
|
1203
|
+
const refresh = react.useCallback(() => {
|
|
1204
|
+
if (room) refreshDevices(room);
|
|
1205
|
+
}, [room, refreshDevices]);
|
|
1195
1206
|
const selectDevice = react.useCallback(
|
|
1196
1207
|
async (kind, deviceId) => {
|
|
1197
1208
|
if (!room) return;
|
|
@@ -1212,6 +1223,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1212
1223
|
alone,
|
|
1213
1224
|
devices,
|
|
1214
1225
|
activeDevices,
|
|
1226
|
+
refreshDevices: refresh,
|
|
1215
1227
|
error,
|
|
1216
1228
|
sfuAvailable,
|
|
1217
1229
|
join,
|
|
@@ -1253,12 +1265,14 @@ function useVoiceRoom(conversationId, opts = {}) {
|
|
|
1253
1265
|
const [error, setError] = react.useState(null);
|
|
1254
1266
|
const optsRef = react.useRef(opts);
|
|
1255
1267
|
const joined = react.useRef(false);
|
|
1268
|
+
const held = react.useRef(null);
|
|
1256
1269
|
const room = react.useMemo(
|
|
1257
1270
|
() => conversationId ? poolse.realtime.voice(conversationId, optsRef.current) : null,
|
|
1258
1271
|
[poolse, conversationId]
|
|
1259
1272
|
);
|
|
1260
1273
|
react.useEffect(() => {
|
|
1261
1274
|
if (!room) return;
|
|
1275
|
+
held.current = room;
|
|
1262
1276
|
setStatus(room.getStatus());
|
|
1263
1277
|
setParticipants(room.getParticipants());
|
|
1264
1278
|
setMuted(room.isMuted());
|
|
@@ -1269,6 +1283,7 @@ function useVoiceRoom(conversationId, opts = {}) {
|
|
|
1269
1283
|
offStatus();
|
|
1270
1284
|
offRoster();
|
|
1271
1285
|
offError();
|
|
1286
|
+
held.current = null;
|
|
1272
1287
|
if (joined.current) {
|
|
1273
1288
|
joined.current = false;
|
|
1274
1289
|
room.leave();
|
|
@@ -1276,10 +1291,15 @@ function useVoiceRoom(conversationId, opts = {}) {
|
|
|
1276
1291
|
};
|
|
1277
1292
|
}, [room]);
|
|
1278
1293
|
const join = react.useCallback(async () => {
|
|
1279
|
-
|
|
1294
|
+
const target = room;
|
|
1295
|
+
if (!target) return;
|
|
1280
1296
|
setError(null);
|
|
1281
1297
|
try {
|
|
1282
|
-
await
|
|
1298
|
+
await target.join();
|
|
1299
|
+
if (held.current !== target) {
|
|
1300
|
+
target.leave();
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1283
1303
|
joined.current = true;
|
|
1284
1304
|
} catch (err) {
|
|
1285
1305
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1305,10 +1325,22 @@ function useCalls(userId, opts = {}) {
|
|
|
1305
1325
|
const ringTimeoutMs = opts.ringTimeoutMs ?? 45e3;
|
|
1306
1326
|
const poolse = usePoolse();
|
|
1307
1327
|
const [phase, setPhase] = react.useState("idle");
|
|
1328
|
+
const phaseRef = react.useRef("idle");
|
|
1329
|
+
phaseRef.current = phase;
|
|
1308
1330
|
const [incoming, setIncoming] = react.useState(null);
|
|
1309
1331
|
const [outgoing, setOutgoing] = react.useState(null);
|
|
1310
1332
|
const [activeConversationId, setActiveConversationId] = react.useState(null);
|
|
1333
|
+
const [activeCallId, setActiveCallId] = react.useState(null);
|
|
1311
1334
|
const [error, setError] = react.useState(null);
|
|
1335
|
+
const incomingRef = react.useRef(null);
|
|
1336
|
+
incomingRef.current = incoming;
|
|
1337
|
+
const outgoingRef = react.useRef(null);
|
|
1338
|
+
outgoingRef.current = outgoing;
|
|
1339
|
+
const activeCallIdRef = react.useRef(null);
|
|
1340
|
+
activeCallIdRef.current = activeCallId;
|
|
1341
|
+
const isCurrent = react.useCallback((callId) => {
|
|
1342
|
+
return callId === activeCallIdRef.current || callId === incomingRef.current?.callId || callId === outgoingRef.current?.callId;
|
|
1343
|
+
}, []);
|
|
1312
1344
|
const actionsRef = react.useRef({
|
|
1313
1345
|
cancel: async () => {
|
|
1314
1346
|
},
|
|
@@ -1319,40 +1351,45 @@ function useCalls(userId, opts = {}) {
|
|
|
1319
1351
|
react.useEffect(() => {
|
|
1320
1352
|
if (!calls) return;
|
|
1321
1353
|
const offIncoming = calls.onIncoming((call2) => {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
return "ringing-in";
|
|
1330
|
-
});
|
|
1354
|
+
if (phaseRef.current !== "idle") {
|
|
1355
|
+
void calls.markBusy(call2).catch(() => {
|
|
1356
|
+
});
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
setIncoming(call2);
|
|
1360
|
+
setPhase("ringing-in");
|
|
1331
1361
|
});
|
|
1332
1362
|
const offAccepted = calls.onAccepted((call2) => {
|
|
1363
|
+
if (outgoingRef.current && call2.callId !== outgoingRef.current.callId) return;
|
|
1333
1364
|
setPhase("active");
|
|
1334
1365
|
setActiveConversationId(call2.conversationId);
|
|
1366
|
+
setActiveCallId(call2.callId);
|
|
1335
1367
|
});
|
|
1336
|
-
const offDeclined = calls.onDeclined(() => {
|
|
1368
|
+
const offDeclined = calls.onDeclined((call2) => {
|
|
1369
|
+
if (!isCurrent(call2.callId)) return;
|
|
1337
1370
|
setPhase("declined");
|
|
1338
1371
|
setOutgoing(null);
|
|
1372
|
+
setActiveCallId(null);
|
|
1339
1373
|
});
|
|
1340
|
-
const offBusy = calls.onBusy(() => {
|
|
1374
|
+
const offBusy = calls.onBusy((call2) => {
|
|
1375
|
+
if (!isCurrent(call2.callId)) return;
|
|
1341
1376
|
setPhase("busy");
|
|
1342
1377
|
setOutgoing(null);
|
|
1378
|
+
setActiveCallId(null);
|
|
1343
1379
|
});
|
|
1344
|
-
const offEnded = calls.onEnded(() => {
|
|
1380
|
+
const offEnded = calls.onEnded((call2) => {
|
|
1381
|
+
if (!isCurrent(call2.callId)) return;
|
|
1345
1382
|
setPhase("idle");
|
|
1346
1383
|
setIncoming(null);
|
|
1347
1384
|
setOutgoing(null);
|
|
1348
1385
|
setActiveConversationId(null);
|
|
1386
|
+
setActiveCallId(null);
|
|
1349
1387
|
});
|
|
1350
1388
|
const offCancelled = calls.onCancelled((call2) => {
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
});
|
|
1389
|
+
if (call2.callId !== incomingRef.current?.callId) return;
|
|
1390
|
+
setIncoming(null);
|
|
1391
|
+
setPhase("idle");
|
|
1392
|
+
setActiveCallId(null);
|
|
1356
1393
|
});
|
|
1357
1394
|
return () => {
|
|
1358
1395
|
offIncoming();
|
|
@@ -1362,7 +1399,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1362
1399
|
offEnded();
|
|
1363
1400
|
offCancelled();
|
|
1364
1401
|
};
|
|
1365
|
-
}, [calls]);
|
|
1402
|
+
}, [calls, isCurrent]);
|
|
1366
1403
|
const call = react.useCallback(
|
|
1367
1404
|
async (conversationId, media2 = "audio") => {
|
|
1368
1405
|
if (!calls) return;
|
|
@@ -1370,6 +1407,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1370
1407
|
try {
|
|
1371
1408
|
const placed = await calls.invite(conversationId, media2);
|
|
1372
1409
|
setOutgoing(placed);
|
|
1410
|
+
setActiveCallId(placed.callId);
|
|
1373
1411
|
setPhase("ringing-out");
|
|
1374
1412
|
} catch (err) {
|
|
1375
1413
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1385,6 +1423,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1385
1423
|
try {
|
|
1386
1424
|
await calls.accept(target);
|
|
1387
1425
|
setActiveConversationId(target.conversationId);
|
|
1426
|
+
setActiveCallId(target.callId);
|
|
1388
1427
|
setPhase("active");
|
|
1389
1428
|
setIncoming(null);
|
|
1390
1429
|
} catch (err) {
|
|
@@ -1403,6 +1442,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1403
1442
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1404
1443
|
} finally {
|
|
1405
1444
|
setIncoming(null);
|
|
1445
|
+
setActiveCallId(null);
|
|
1406
1446
|
setPhase("idle");
|
|
1407
1447
|
}
|
|
1408
1448
|
},
|
|
@@ -1416,6 +1456,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1416
1456
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1417
1457
|
} finally {
|
|
1418
1458
|
setOutgoing(null);
|
|
1459
|
+
setActiveCallId(null);
|
|
1419
1460
|
setPhase("idle");
|
|
1420
1461
|
}
|
|
1421
1462
|
}, [calls, outgoing]);
|
|
@@ -1433,7 +1474,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1433
1474
|
}, [phase, ringTimeoutMs]);
|
|
1434
1475
|
const hangUp = react.useCallback(async () => {
|
|
1435
1476
|
const conversationId = activeConversationId ?? incoming?.conversationId ?? outgoing?.conversationId;
|
|
1436
|
-
const callId = incoming?.callId ?? outgoing?.callId;
|
|
1477
|
+
const callId = activeCallId ?? incoming?.callId ?? outgoing?.callId;
|
|
1437
1478
|
try {
|
|
1438
1479
|
if (!calls || !callId || !conversationId) return;
|
|
1439
1480
|
if (phase === "ringing-out" && outgoing) {
|
|
@@ -1450,13 +1491,15 @@ function useCalls(userId, opts = {}) {
|
|
|
1450
1491
|
setIncoming(null);
|
|
1451
1492
|
setOutgoing(null);
|
|
1452
1493
|
setActiveConversationId(null);
|
|
1494
|
+
setActiveCallId(null);
|
|
1453
1495
|
}
|
|
1454
|
-
}, [calls, phase, incoming, outgoing, activeConversationId]);
|
|
1496
|
+
}, [calls, phase, incoming, outgoing, activeConversationId, activeCallId]);
|
|
1455
1497
|
const reset = react.useCallback(() => {
|
|
1456
1498
|
setPhase("idle");
|
|
1457
1499
|
setIncoming(null);
|
|
1458
1500
|
setOutgoing(null);
|
|
1459
1501
|
setActiveConversationId(null);
|
|
1502
|
+
setActiveCallId(null);
|
|
1460
1503
|
setError(null);
|
|
1461
1504
|
}, []);
|
|
1462
1505
|
const media = incoming?.media ?? outgoing?.media ?? "audio";
|