@poolse/react 2.18.0 → 2.19.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 +34 -0
- package/dist/index.cjs +75 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +75 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ 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.19.0] — 2026-08-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **A call cancelled by the caller left the callee's phone ringing.**
|
|
12
|
+
A ring delivered by VoIP push was shown by CallKit without the state
|
|
13
|
+
machine ever hearing about it, so `call:cancelled` was matched against
|
|
14
|
+
an `incoming` that had never been set, failed the guard, and did
|
|
15
|
+
nothing. The phone rang on until its own 45-second timeout. New
|
|
16
|
+
`ring()` registers a ring that arrived off-socket; `onIncoming` now
|
|
17
|
+
recognises the same call arriving by both routes instead of answering
|
|
18
|
+
your own ring with `call:busy`.
|
|
19
|
+
|
|
20
|
+
- **Declining a call before the socket was ready told nobody.** The
|
|
21
|
+
hook builds nothing until it has a user id, and host apps get that
|
|
22
|
+
from `useMe()` — a network round trip. A VoIP push launches the app
|
|
23
|
+
cold and CallKit lets you answer or refuse from the lock screen well
|
|
24
|
+
before that returns, so both actions hit `if (!calls) return` and were
|
|
25
|
+
dropped. They are now queued and sent the moment the resource exists.
|
|
26
|
+
`accept()` had the identical hole.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **A group ring no longer ends on the first refusal.** Declines and
|
|
31
|
+
busies accumulate per callee, and the call ends only once nobody is
|
|
32
|
+
left who might still pick up. All busy reports busy; any outright
|
|
33
|
+
refusal reports declined. A decline arriving after somebody answered
|
|
34
|
+
no longer ends the active call.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `dropped(callId)` — a call torn down from outside the state machine,
|
|
39
|
+
which is how the end of a call reaches a device whose socket isn't up.
|
|
40
|
+
|
|
7
41
|
## [2.18.0] — 2026-08-24
|
|
8
42
|
|
|
9
43
|
Republished in lockstep.
|
package/dist/index.cjs
CHANGED
|
@@ -1342,6 +1342,8 @@ function useCalls(userId, opts = {}) {
|
|
|
1342
1342
|
const isCurrent = react.useCallback((callId) => {
|
|
1343
1343
|
return callId === activeCallIdRef.current || callId === incomingRef.current?.callId || callId === outgoingRef.current?.callId;
|
|
1344
1344
|
}, []);
|
|
1345
|
+
const rejectedRef = react.useRef(/* @__PURE__ */ new Map());
|
|
1346
|
+
const pendingRef = react.useRef(null);
|
|
1345
1347
|
const actionsRef = react.useRef({
|
|
1346
1348
|
cancel: async () => {
|
|
1347
1349
|
},
|
|
@@ -1349,9 +1351,24 @@ function useCalls(userId, opts = {}) {
|
|
|
1349
1351
|
}
|
|
1350
1352
|
});
|
|
1351
1353
|
const calls = react.useMemo(() => userId ? poolse.realtime.calls(userId) : null, [poolse, userId]);
|
|
1354
|
+
const reject = react.useCallback((callId, userId2, kind) => {
|
|
1355
|
+
if (phaseRef.current !== "ringing-out") return;
|
|
1356
|
+
const out = outgoingRef.current;
|
|
1357
|
+
if (!out || out.callId !== callId) return;
|
|
1358
|
+
rejectedRef.current.set(userId2, kind);
|
|
1359
|
+
const outstanding = out.calleeUserIds.filter((id) => !rejectedRef.current.has(id));
|
|
1360
|
+
if (out.calleeUserIds.length > 0 && outstanding.length > 0) return;
|
|
1361
|
+
const allBusy = [...rejectedRef.current.values()].every((k) => k === "busy");
|
|
1362
|
+
setPhase(allBusy ? "busy" : "declined");
|
|
1363
|
+
setOutgoing(null);
|
|
1364
|
+
setActiveCallId(null);
|
|
1365
|
+
setActiveMedia(null);
|
|
1366
|
+
}, []);
|
|
1352
1367
|
react.useEffect(() => {
|
|
1353
1368
|
if (!calls) return;
|
|
1354
1369
|
const offIncoming = calls.onIncoming((call2) => {
|
|
1370
|
+
if (call2.callId === incomingRef.current?.callId) return;
|
|
1371
|
+
if (call2.callId === activeCallIdRef.current) return;
|
|
1355
1372
|
if (phaseRef.current !== "idle") {
|
|
1356
1373
|
void calls.markBusy(call2).catch(() => {
|
|
1357
1374
|
});
|
|
@@ -1368,17 +1385,11 @@ function useCalls(userId, opts = {}) {
|
|
|
1368
1385
|
});
|
|
1369
1386
|
const offDeclined = calls.onDeclined((call2) => {
|
|
1370
1387
|
if (!isCurrent(call2.callId)) return;
|
|
1371
|
-
|
|
1372
|
-
setOutgoing(null);
|
|
1373
|
-
setActiveCallId(null);
|
|
1374
|
-
setActiveMedia(null);
|
|
1388
|
+
reject(call2.callId, call2.userId, "declined");
|
|
1375
1389
|
});
|
|
1376
1390
|
const offBusy = calls.onBusy((call2) => {
|
|
1377
1391
|
if (!isCurrent(call2.callId)) return;
|
|
1378
|
-
|
|
1379
|
-
setOutgoing(null);
|
|
1380
|
-
setActiveCallId(null);
|
|
1381
|
-
setActiveMedia(null);
|
|
1392
|
+
reject(call2.callId, call2.userId, "busy");
|
|
1382
1393
|
});
|
|
1383
1394
|
const offEnded = calls.onEnded((call2) => {
|
|
1384
1395
|
if (!isCurrent(call2.callId)) return;
|
|
@@ -1404,11 +1415,22 @@ function useCalls(userId, opts = {}) {
|
|
|
1404
1415
|
offEnded();
|
|
1405
1416
|
offCancelled();
|
|
1406
1417
|
};
|
|
1407
|
-
}, [calls, isCurrent]);
|
|
1418
|
+
}, [calls, isCurrent, reject]);
|
|
1419
|
+
react.useEffect(() => {
|
|
1420
|
+
if (!calls) return;
|
|
1421
|
+
const queued = pendingRef.current;
|
|
1422
|
+
if (!queued) return;
|
|
1423
|
+
pendingRef.current = null;
|
|
1424
|
+
const sent = queued.kind === "accept" ? calls.accept(queued.call) : calls.decline(queued.call);
|
|
1425
|
+
void sent.catch((err) => {
|
|
1426
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1427
|
+
});
|
|
1428
|
+
}, [calls]);
|
|
1408
1429
|
const call = react.useCallback(
|
|
1409
1430
|
async (conversationId, media2 = "audio") => {
|
|
1410
1431
|
if (!calls) return;
|
|
1411
1432
|
setError(null);
|
|
1433
|
+
rejectedRef.current = /* @__PURE__ */ new Map();
|
|
1412
1434
|
try {
|
|
1413
1435
|
const placed = await calls.invite(conversationId, media2);
|
|
1414
1436
|
setOutgoing(placed);
|
|
@@ -1422,10 +1444,40 @@ function useCalls(userId, opts = {}) {
|
|
|
1422
1444
|
},
|
|
1423
1445
|
[calls]
|
|
1424
1446
|
);
|
|
1447
|
+
const ring = react.useCallback((call2) => {
|
|
1448
|
+
if (incomingRef.current?.callId === call2.callId) return;
|
|
1449
|
+
if (activeCallIdRef.current === call2.callId) return;
|
|
1450
|
+
if (phaseRef.current !== "idle") return;
|
|
1451
|
+
setIncoming(call2);
|
|
1452
|
+
setPhase("ringing-in");
|
|
1453
|
+
}, []);
|
|
1454
|
+
const dropped = react.useCallback(
|
|
1455
|
+
(callId) => {
|
|
1456
|
+
if (pendingRef.current?.call.callId === callId) pendingRef.current = null;
|
|
1457
|
+
if (!isCurrent(callId)) return;
|
|
1458
|
+
rejectedRef.current = /* @__PURE__ */ new Map();
|
|
1459
|
+
setPhase("idle");
|
|
1460
|
+
setIncoming(null);
|
|
1461
|
+
setOutgoing(null);
|
|
1462
|
+
setActiveConversationId(null);
|
|
1463
|
+
setActiveCallId(null);
|
|
1464
|
+
setActiveMedia(null);
|
|
1465
|
+
},
|
|
1466
|
+
[isCurrent]
|
|
1467
|
+
);
|
|
1425
1468
|
const accept = react.useCallback(
|
|
1426
1469
|
async (call2) => {
|
|
1427
1470
|
const target = call2 ?? incoming;
|
|
1428
|
-
if (!
|
|
1471
|
+
if (!target) return;
|
|
1472
|
+
if (!calls) {
|
|
1473
|
+
pendingRef.current = { kind: "accept", call: target };
|
|
1474
|
+
setActiveConversationId(target.conversationId);
|
|
1475
|
+
setActiveCallId(target.callId);
|
|
1476
|
+
setActiveMedia(target.media);
|
|
1477
|
+
setPhase("active");
|
|
1478
|
+
setIncoming(null);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1429
1481
|
try {
|
|
1430
1482
|
await calls.accept(target);
|
|
1431
1483
|
setActiveConversationId(target.conversationId);
|
|
@@ -1442,7 +1494,15 @@ function useCalls(userId, opts = {}) {
|
|
|
1442
1494
|
const decline = react.useCallback(
|
|
1443
1495
|
async (call2) => {
|
|
1444
1496
|
const target = call2 ?? incoming;
|
|
1445
|
-
if (!
|
|
1497
|
+
if (!target) return;
|
|
1498
|
+
if (!calls) {
|
|
1499
|
+
pendingRef.current = { kind: "decline", call: target };
|
|
1500
|
+
setIncoming(null);
|
|
1501
|
+
setActiveCallId(null);
|
|
1502
|
+
setActiveMedia(null);
|
|
1503
|
+
setPhase("idle");
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1446
1506
|
try {
|
|
1447
1507
|
await calls.decline(target);
|
|
1448
1508
|
} catch (err) {
|
|
@@ -1505,6 +1565,8 @@ function useCalls(userId, opts = {}) {
|
|
|
1505
1565
|
}
|
|
1506
1566
|
}, [calls, phase, incoming, outgoing, activeConversationId, activeCallId]);
|
|
1507
1567
|
const reset = react.useCallback(() => {
|
|
1568
|
+
rejectedRef.current = /* @__PURE__ */ new Map();
|
|
1569
|
+
pendingRef.current = null;
|
|
1508
1570
|
setPhase("idle");
|
|
1509
1571
|
setIncoming(null);
|
|
1510
1572
|
setOutgoing(null);
|
|
@@ -1522,6 +1584,8 @@ function useCalls(userId, opts = {}) {
|
|
|
1522
1584
|
media,
|
|
1523
1585
|
error,
|
|
1524
1586
|
call,
|
|
1587
|
+
ring,
|
|
1588
|
+
dropped,
|
|
1525
1589
|
accept,
|
|
1526
1590
|
decline,
|
|
1527
1591
|
cancel,
|