@poolse/react-native 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 CHANGED
@@ -4,6 +4,33 @@ All notable changes to `@poolse/react-native` are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
5
  versions follow [semver](https://semver.org).
6
6
 
7
+ ## [2.19.0] — 2026-08-25
8
+
9
+ ### Added
10
+
11
+ - **The end of a call now travels by push, like its start.** A device
12
+ woken by a VoIP push is ringing on a lock screen with a socket that
13
+ may not be connected, so the broadcast that ends a call reaches
14
+ nothing there. `useVoipCalls` now understands a teardown push
15
+ (`event: "ended"`) and tears the ring down instead of starting one.
16
+ Needs a server that sends them, and an AppDelegate that reports the
17
+ call before ending it — see the sample's `plugins/withVoipPush.js`.
18
+
19
+ - `readPush()` — exported and pure, so what a push MEANS can be tested
20
+ apart from what CallKit does about it. A missing `event` reads as a
21
+ ring, which is what servers predating the field send.
22
+
23
+ ### Fixed
24
+
25
+ - **A ring that arrived by push is now registered with `useCalls`**, so
26
+ every signal that ends a call can match it. Previously only CallKit
27
+ knew about it. See `@poolse/react` 2.19.0.
28
+
29
+ ### Changed
30
+
31
+ - A finished call — declined, busy, no answer — closes itself after a
32
+ short pause instead of waiting behind a Close button.
33
+
7
34
  ## [2.18.0] — 2026-08-24
8
35
 
9
36
  ### Fixed
package/dist/index.cjs CHANGED
@@ -5836,6 +5836,10 @@ function format(total) {
5836
5836
  return `${hours > 0 ? `${hours}:` : ""}${mm}:${String(seconds).padStart(2, "0")}`;
5837
5837
  }
5838
5838
  var CHROME_IDLE_MS = 4500;
5839
+ var ENDED_LINGER_MS = 2500;
5840
+ function isOver(phase) {
5841
+ return phase === "declined" || phase === "busy" || phase === "timed-out";
5842
+ }
5839
5843
  function CallScreen({
5840
5844
  userId,
5841
5845
  labelFor,
@@ -5951,6 +5955,11 @@ function CallScreen({
5951
5955
  endedOnEmpty.current = true;
5952
5956
  hangUp();
5953
5957
  }, [alone, calls.phase, hangUp]);
5958
+ react.useEffect(() => {
5959
+ if (!isOver(calls.phase)) return;
5960
+ const timer = setTimeout(calls.reset, ENDED_LINGER_MS);
5961
+ return () => clearTimeout(timer);
5962
+ }, [calls.phase, calls.reset]);
5954
5963
  const micOn = usingSfu ? sfu.micEnabled : !mesh.muted;
5955
5964
  const toggleMic = react.useCallback(() => {
5956
5965
  if (usingSfu) void sfu.toggleMic();
@@ -5964,7 +5973,7 @@ function CallScreen({
5964
5973
  const label = react.useMemo(() => labelFor ?? ((id) => `User ${id.slice(0, 6)}`), [labelFor]);
5965
5974
  const ringingIn = calls.phase === "ringing-in";
5966
5975
  const ringingOut = calls.phase === "ringing-out";
5967
- const ended = calls.phase === "declined" || calls.phase === "busy" || calls.phase === "timed-out";
5976
+ const ended = isOver(calls.phase);
5968
5977
  const visible = calls.phase !== "idle" && !(nativeIncomingUi && ringingIn);
5969
5978
  const duration = useCallDuration(connected);
5970
5979
  const participants = usingSfu ? sfu.participants : [];
@@ -6206,6 +6215,26 @@ function isStalePush(sentAt, now, staleAfterMs) {
6206
6215
  if (age < 0) return false;
6207
6216
  return age > staleAfterMs;
6208
6217
  }
6218
+ function readPush(payload) {
6219
+ const callId = payload.call_id;
6220
+ if (!callId) return { kind: "ignore" };
6221
+ if (payload.event === "ended") return { kind: "teardown", callId };
6222
+ const conversationId = payload.conversation_id;
6223
+ if (!conversationId) return { kind: "ignore" };
6224
+ return {
6225
+ kind: "ring",
6226
+ sentAt: payload.sent_at,
6227
+ call: {
6228
+ callId,
6229
+ conversationId,
6230
+ callerUserId: payload.caller_user_id ?? "",
6231
+ callerName: payload.caller_name ?? "Incoming call",
6232
+ // `has_video` is what older servers sent; `media` supersedes it.
6233
+ // Either saying video means the system UI offers video.
6234
+ media: payload.media === "video" || payload.has_video ? "video" : "audio"
6235
+ }
6236
+ };
6237
+ }
6209
6238
  function useVoipCalls({
6210
6239
  voipPush,
6211
6240
  callKeep,
@@ -6277,21 +6306,20 @@ function useVoipCalls({
6277
6306
  react.useEffect(() => {
6278
6307
  let cancelled = false;
6279
6308
  const fromPush = (payload) => {
6280
- const callId = payload.call_id;
6281
- const conversationId = payload.conversation_id;
6282
- if (!callId || !conversationId) return;
6283
- present.current(
6284
- {
6285
- callId,
6286
- conversationId,
6287
- callerUserId: payload.caller_user_id ?? "",
6288
- callerName: payload.caller_name ?? "Incoming call",
6289
- // `has_video` is what older servers sent; `media` supersedes
6290
- // it. Either says video, and the system UI offers video.
6291
- media: payload.media === "video" || payload.has_video ? "video" : "audio"
6292
- },
6293
- payload.sent_at
6294
- );
6309
+ const intent = readPush(payload);
6310
+ if (intent.kind === "ignore") return;
6311
+ if (intent.kind === "teardown") {
6312
+ dismiss.current(intent.callId);
6313
+ handlers.current.calls?.dropped(intent.callId);
6314
+ return;
6315
+ }
6316
+ present.current(intent.call, intent.sentAt);
6317
+ handlers.current.calls?.ring({
6318
+ callId: intent.call.callId,
6319
+ conversationId: intent.call.conversationId,
6320
+ callerUserId: intent.call.callerUserId,
6321
+ media: intent.call.media
6322
+ });
6295
6323
  };
6296
6324
  ready.current = callKeep.setup({
6297
6325
  ios: { appName },