@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/dist/index.d.cts CHANGED
@@ -532,6 +532,17 @@ interface UseCallRoom {
532
532
  activeDevices: Partial<Record<CallDeviceKind, string>>;
533
533
  /** Move the call onto another camera, microphone or output. */
534
534
  selectDevice: (kind: CallDeviceKind, deviceId: string) => Promise<void>;
535
+ /**
536
+ * Re-read the device list.
537
+ *
538
+ * Called for you on connect and whenever devices change, so this is
539
+ * for the case neither covers: a browser only reveals device ids and
540
+ * labels once the page holds a matching permission, and the moment
541
+ * that permission lands is not an event you can subscribe to. Call it
542
+ * when opening a picker and the list will be right by the time it is
543
+ * on screen.
544
+ */
545
+ refreshDevices: () => void;
535
546
  /**
536
547
  * Everyone else has gone and is not coming back.
537
548
  *
package/dist/index.d.ts CHANGED
@@ -532,6 +532,17 @@ interface UseCallRoom {
532
532
  activeDevices: Partial<Record<CallDeviceKind, string>>;
533
533
  /** Move the call onto another camera, microphone or output. */
534
534
  selectDevice: (kind: CallDeviceKind, deviceId: string) => Promise<void>;
535
+ /**
536
+ * Re-read the device list.
537
+ *
538
+ * Called for you on connect and whenever devices change, so this is
539
+ * for the case neither covers: a browser only reveals device ids and
540
+ * labels once the page holds a matching permission, and the moment
541
+ * that permission lands is not an event you can subscribe to. Call it
542
+ * when opening a picker and the list will be right by the time it is
543
+ * on screen.
544
+ */
545
+ refreshDevices: () => void;
535
546
  /**
536
547
  * Everyone else has gone and is not coming back.
537
548
  *
package/dist/index.js CHANGED
@@ -1081,6 +1081,7 @@ function useCallRoom(conversationId, opts) {
1081
1081
  const optsRef = useRef(opts);
1082
1082
  optsRef.current = opts;
1083
1083
  const joined = useRef(false);
1084
+ const held = useRef(null);
1084
1085
  const room = useMemo(() => {
1085
1086
  if (!conversationId) return null;
1086
1087
  return poolse.call(conversationId, {
@@ -1120,6 +1121,7 @@ function useCallRoom(conversationId, opts) {
1120
1121
  }
1121
1122
  setStatus(room.getStatus());
1122
1123
  setParticipants(room.getParticipants());
1124
+ held.current = room;
1123
1125
  setAlone(false);
1124
1126
  const offStatus = room.onStatus(setStatus);
1125
1127
  const offRoster = room.onParticipants((next) => {
@@ -1135,6 +1137,7 @@ function useCallRoom(conversationId, opts) {
1135
1137
  offAlone();
1136
1138
  offDevices();
1137
1139
  offError();
1140
+ held.current = null;
1138
1141
  if (joined.current) {
1139
1142
  joined.current = false;
1140
1143
  room.leave();
@@ -1142,14 +1145,19 @@ function useCallRoom(conversationId, opts) {
1142
1145
  };
1143
1146
  }, [room, sync, refreshDevices]);
1144
1147
  const join = useCallback(async () => {
1145
- if (!room) return false;
1148
+ const target = room;
1149
+ if (!target) return false;
1146
1150
  setError(null);
1147
1151
  try {
1148
- const connected = await room.join();
1152
+ const connected = await target.join();
1153
+ if (held.current !== target) {
1154
+ if (connected) target.leave();
1155
+ return false;
1156
+ }
1149
1157
  joined.current = connected;
1150
1158
  setSfuAvailable(connected);
1151
- sync(room);
1152
- if (connected) refreshDevices(room);
1159
+ sync(target);
1160
+ if (connected) refreshDevices(target);
1153
1161
  return connected;
1154
1162
  } catch (err) {
1155
1163
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1191,6 +1199,9 @@ function useCallRoom(conversationId, opts) {
1191
1199
  await room.switchCamera();
1192
1200
  setFacing(room.getFacingMode());
1193
1201
  }, [room]);
1202
+ const refresh = useCallback(() => {
1203
+ if (room) refreshDevices(room);
1204
+ }, [room, refreshDevices]);
1194
1205
  const selectDevice = useCallback(
1195
1206
  async (kind, deviceId) => {
1196
1207
  if (!room) return;
@@ -1211,6 +1222,7 @@ function useCallRoom(conversationId, opts) {
1211
1222
  alone,
1212
1223
  devices,
1213
1224
  activeDevices,
1225
+ refreshDevices: refresh,
1214
1226
  error,
1215
1227
  sfuAvailable,
1216
1228
  join,
@@ -1252,12 +1264,14 @@ function useVoiceRoom(conversationId, opts = {}) {
1252
1264
  const [error, setError] = useState(null);
1253
1265
  const optsRef = useRef(opts);
1254
1266
  const joined = useRef(false);
1267
+ const held = useRef(null);
1255
1268
  const room = useMemo(
1256
1269
  () => conversationId ? poolse.realtime.voice(conversationId, optsRef.current) : null,
1257
1270
  [poolse, conversationId]
1258
1271
  );
1259
1272
  useEffect(() => {
1260
1273
  if (!room) return;
1274
+ held.current = room;
1261
1275
  setStatus(room.getStatus());
1262
1276
  setParticipants(room.getParticipants());
1263
1277
  setMuted(room.isMuted());
@@ -1268,6 +1282,7 @@ function useVoiceRoom(conversationId, opts = {}) {
1268
1282
  offStatus();
1269
1283
  offRoster();
1270
1284
  offError();
1285
+ held.current = null;
1271
1286
  if (joined.current) {
1272
1287
  joined.current = false;
1273
1288
  room.leave();
@@ -1275,10 +1290,15 @@ function useVoiceRoom(conversationId, opts = {}) {
1275
1290
  };
1276
1291
  }, [room]);
1277
1292
  const join = useCallback(async () => {
1278
- if (!room) return;
1293
+ const target = room;
1294
+ if (!target) return;
1279
1295
  setError(null);
1280
1296
  try {
1281
- await room.join();
1297
+ await target.join();
1298
+ if (held.current !== target) {
1299
+ target.leave();
1300
+ return;
1301
+ }
1282
1302
  joined.current = true;
1283
1303
  } catch (err) {
1284
1304
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1304,10 +1324,22 @@ function useCalls(userId, opts = {}) {
1304
1324
  const ringTimeoutMs = opts.ringTimeoutMs ?? 45e3;
1305
1325
  const poolse = usePoolse();
1306
1326
  const [phase, setPhase] = useState("idle");
1327
+ const phaseRef = useRef("idle");
1328
+ phaseRef.current = phase;
1307
1329
  const [incoming, setIncoming] = useState(null);
1308
1330
  const [outgoing, setOutgoing] = useState(null);
1309
1331
  const [activeConversationId, setActiveConversationId] = useState(null);
1332
+ const [activeCallId, setActiveCallId] = useState(null);
1310
1333
  const [error, setError] = useState(null);
1334
+ const incomingRef = useRef(null);
1335
+ incomingRef.current = incoming;
1336
+ const outgoingRef = useRef(null);
1337
+ outgoingRef.current = outgoing;
1338
+ const activeCallIdRef = useRef(null);
1339
+ activeCallIdRef.current = activeCallId;
1340
+ const isCurrent = useCallback((callId) => {
1341
+ return callId === activeCallIdRef.current || callId === incomingRef.current?.callId || callId === outgoingRef.current?.callId;
1342
+ }, []);
1311
1343
  const actionsRef = useRef({
1312
1344
  cancel: async () => {
1313
1345
  },
@@ -1318,40 +1350,45 @@ function useCalls(userId, opts = {}) {
1318
1350
  useEffect(() => {
1319
1351
  if (!calls) return;
1320
1352
  const offIncoming = calls.onIncoming((call2) => {
1321
- setPhase((current) => {
1322
- if (current !== "idle") {
1323
- void calls.markBusy(call2).catch(() => {
1324
- });
1325
- return current;
1326
- }
1327
- setIncoming(call2);
1328
- return "ringing-in";
1329
- });
1353
+ if (phaseRef.current !== "idle") {
1354
+ void calls.markBusy(call2).catch(() => {
1355
+ });
1356
+ return;
1357
+ }
1358
+ setIncoming(call2);
1359
+ setPhase("ringing-in");
1330
1360
  });
1331
1361
  const offAccepted = calls.onAccepted((call2) => {
1362
+ if (outgoingRef.current && call2.callId !== outgoingRef.current.callId) return;
1332
1363
  setPhase("active");
1333
1364
  setActiveConversationId(call2.conversationId);
1365
+ setActiveCallId(call2.callId);
1334
1366
  });
1335
- const offDeclined = calls.onDeclined(() => {
1367
+ const offDeclined = calls.onDeclined((call2) => {
1368
+ if (!isCurrent(call2.callId)) return;
1336
1369
  setPhase("declined");
1337
1370
  setOutgoing(null);
1371
+ setActiveCallId(null);
1338
1372
  });
1339
- const offBusy = calls.onBusy(() => {
1373
+ const offBusy = calls.onBusy((call2) => {
1374
+ if (!isCurrent(call2.callId)) return;
1340
1375
  setPhase("busy");
1341
1376
  setOutgoing(null);
1377
+ setActiveCallId(null);
1342
1378
  });
1343
- const offEnded = calls.onEnded(() => {
1379
+ const offEnded = calls.onEnded((call2) => {
1380
+ if (!isCurrent(call2.callId)) return;
1344
1381
  setPhase("idle");
1345
1382
  setIncoming(null);
1346
1383
  setOutgoing(null);
1347
1384
  setActiveConversationId(null);
1385
+ setActiveCallId(null);
1348
1386
  });
1349
1387
  const offCancelled = calls.onCancelled((call2) => {
1350
- setIncoming((current) => {
1351
- if (current && current.callId !== call2.callId) return current;
1352
- setPhase("idle");
1353
- return null;
1354
- });
1388
+ if (call2.callId !== incomingRef.current?.callId) return;
1389
+ setIncoming(null);
1390
+ setPhase("idle");
1391
+ setActiveCallId(null);
1355
1392
  });
1356
1393
  return () => {
1357
1394
  offIncoming();
@@ -1361,7 +1398,7 @@ function useCalls(userId, opts = {}) {
1361
1398
  offEnded();
1362
1399
  offCancelled();
1363
1400
  };
1364
- }, [calls]);
1401
+ }, [calls, isCurrent]);
1365
1402
  const call = useCallback(
1366
1403
  async (conversationId, media2 = "audio") => {
1367
1404
  if (!calls) return;
@@ -1369,6 +1406,7 @@ function useCalls(userId, opts = {}) {
1369
1406
  try {
1370
1407
  const placed = await calls.invite(conversationId, media2);
1371
1408
  setOutgoing(placed);
1409
+ setActiveCallId(placed.callId);
1372
1410
  setPhase("ringing-out");
1373
1411
  } catch (err) {
1374
1412
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1384,6 +1422,7 @@ function useCalls(userId, opts = {}) {
1384
1422
  try {
1385
1423
  await calls.accept(target);
1386
1424
  setActiveConversationId(target.conversationId);
1425
+ setActiveCallId(target.callId);
1387
1426
  setPhase("active");
1388
1427
  setIncoming(null);
1389
1428
  } catch (err) {
@@ -1402,6 +1441,7 @@ function useCalls(userId, opts = {}) {
1402
1441
  setError(err instanceof Error ? err : new Error(String(err)));
1403
1442
  } finally {
1404
1443
  setIncoming(null);
1444
+ setActiveCallId(null);
1405
1445
  setPhase("idle");
1406
1446
  }
1407
1447
  },
@@ -1415,6 +1455,7 @@ function useCalls(userId, opts = {}) {
1415
1455
  setError(err instanceof Error ? err : new Error(String(err)));
1416
1456
  } finally {
1417
1457
  setOutgoing(null);
1458
+ setActiveCallId(null);
1418
1459
  setPhase("idle");
1419
1460
  }
1420
1461
  }, [calls, outgoing]);
@@ -1432,7 +1473,7 @@ function useCalls(userId, opts = {}) {
1432
1473
  }, [phase, ringTimeoutMs]);
1433
1474
  const hangUp = useCallback(async () => {
1434
1475
  const conversationId = activeConversationId ?? incoming?.conversationId ?? outgoing?.conversationId;
1435
- const callId = incoming?.callId ?? outgoing?.callId;
1476
+ const callId = activeCallId ?? incoming?.callId ?? outgoing?.callId;
1436
1477
  try {
1437
1478
  if (!calls || !callId || !conversationId) return;
1438
1479
  if (phase === "ringing-out" && outgoing) {
@@ -1449,13 +1490,15 @@ function useCalls(userId, opts = {}) {
1449
1490
  setIncoming(null);
1450
1491
  setOutgoing(null);
1451
1492
  setActiveConversationId(null);
1493
+ setActiveCallId(null);
1452
1494
  }
1453
- }, [calls, phase, incoming, outgoing, activeConversationId]);
1495
+ }, [calls, phase, incoming, outgoing, activeConversationId, activeCallId]);
1454
1496
  const reset = useCallback(() => {
1455
1497
  setPhase("idle");
1456
1498
  setIncoming(null);
1457
1499
  setOutgoing(null);
1458
1500
  setActiveConversationId(null);
1501
+ setActiveCallId(null);
1459
1502
  setError(null);
1460
1503
  }, []);
1461
1504
  const media = incoming?.media ?? outgoing?.media ?? "audio";