Gate native CallStyle dismiss on App.isActive so background incoming rings keep ringing instead of being prematurely cleared by /sync atom ADD.
This commit is contained in:
parent
2836411830
commit
be2019daeb
1 changed files with 101 additions and 33 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { App } from '@capacitor/app';
|
||||||
import { incomingCallsAtom } from '../state/incomingCalls';
|
import { incomingCallsAtom } from '../state/incomingCalls';
|
||||||
import { isNativePlatform } from '../utils/capacitor';
|
import { isNativePlatform } from '../utils/capacitor';
|
||||||
|
|
||||||
|
|
@ -17,51 +18,118 @@ function javaStringHashCode(s: string): number {
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep native CallStyle in sync with the JS-owned incoming-calls atom. When JS
|
async function dismissRooms(roomIds: string[]): Promise<void> {
|
||||||
// either loses a ring (accepted, declined, other device joined, lifetime
|
if (roomIds.length === 0) return;
|
||||||
// expired) OR newly takes ownership of one after the app returns foreground,
|
if (!isNativePlatform()) return;
|
||||||
// clear the system notification for that room. The AlarmManager fallback in the
|
const { PushNotifications } = await import('@capacitor/push-notifications');
|
||||||
// Java service still handles killed-process dismiss on lifetime expiry.
|
// Shape mirrors VojoFirebaseMessagingService.showIncomingCallNotification:
|
||||||
|
// tag = "call_" + roomId, id = tag.hashCode(). The Android Capacitor
|
||||||
|
// plugin reads id via JSObject.getInteger (PushNotificationsPlugin.java
|
||||||
|
// line 166), so the id must go over the bridge as a number — the TS
|
||||||
|
// type declares string, hence the cast.
|
||||||
|
const notifications = roomIds.map((roomId) => {
|
||||||
|
const tag = `call_${roomId}`;
|
||||||
|
let id = javaStringHashCode(tag);
|
||||||
|
if (id === SUMMARY_NOTIFICATION_ID) id += 1;
|
||||||
|
return { id, tag };
|
||||||
|
});
|
||||||
|
await PushNotifications.removeDeliveredNotifications({
|
||||||
|
notifications: notifications as unknown as Parameters<
|
||||||
|
typeof PushNotifications.removeDeliveredNotifications
|
||||||
|
>[0]['notifications'],
|
||||||
|
}).catch(() => {
|
||||||
|
/* best-effort — alarm fallback still dismisses at lifetime expiry */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep native CallStyle in sync with the JS-owned incoming-calls atom, enforcing
|
||||||
|
// the "foreground → JS strip, background → platform surface" invariant on the
|
||||||
|
// dismiss side (symmetric to the show-side skip in VojoFirebaseMessagingService
|
||||||
|
// and the JS-audio gate in IncomingCallStripRenderer).
|
||||||
|
// - REMOVE (ring ended: accept/decline/other-device/expiry) → dismiss always.
|
||||||
|
// - ADD while foregrounded → JS strip owns UX → dismiss any stale native.
|
||||||
|
// - ADD while backgrounded → native CallStyle owns UX → keep it, do NOT dismiss.
|
||||||
|
// - background → foreground transition with a live ring → one-shot sweep hands
|
||||||
|
// ownership back to the JS strip.
|
||||||
|
// The AlarmManager fallback in the Java service still handles killed-process
|
||||||
|
// dismiss on lifetime expiry.
|
||||||
export const useDismissNativeCallNotifications = (): void => {
|
export const useDismissNativeCallNotifications = (): void => {
|
||||||
const incoming = useAtomValue(incomingCallsAtom);
|
const incoming = useAtomValue(incomingCallsAtom);
|
||||||
const prevRoomsRef = useRef<Set<string>>(new Set());
|
const prevRoomsRef = useRef<Set<string>>(new Set());
|
||||||
|
const [appActive, setAppActive] = useState(
|
||||||
|
() => typeof document === 'undefined' || document.visibilityState === 'visible'
|
||||||
|
);
|
||||||
|
|
||||||
|
// App state tracking. Race-guard: a real appStateChange firing before
|
||||||
|
// getState() resolves must win, otherwise the stale snapshot re-enables the
|
||||||
|
// foreground branch while we are actually backgrounded (and we'd dismiss
|
||||||
|
// the native ring we just raised).
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
let sawAppStateEvent = false;
|
||||||
|
let remove: (() => void) | undefined;
|
||||||
|
|
||||||
|
App.addListener('appStateChange', ({ isActive }) => {
|
||||||
|
sawAppStateEvent = true;
|
||||||
|
if (!cancelled) setAppActive(isActive);
|
||||||
|
}).then((handle) => {
|
||||||
|
if (cancelled) handle.remove();
|
||||||
|
else
|
||||||
|
remove = () => {
|
||||||
|
handle.remove();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
App.getState()
|
||||||
|
.then((state) => {
|
||||||
|
if (!cancelled && !sawAppStateEvent) setAppActive(state.isActive);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* web fallback handled by initial document.visibilityState */
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
remove?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Diff-driven dismiss. prevRoomsRef is the write-site for the current-rooms
|
||||||
|
// snapshot; the handoff-effect below reads it and relies on this effect
|
||||||
|
// running first on shared re-renders (React runs effects in declaration
|
||||||
|
// order — do not reorder).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextRooms = new Set<string>();
|
const nextRooms = new Set<string>();
|
||||||
incoming.forEach((call) => nextRooms.add(call.roomId));
|
incoming.forEach((call) => nextRooms.add(call.roomId));
|
||||||
|
|
||||||
const changed: string[] = [];
|
const added: string[] = [];
|
||||||
|
const removed: string[] = [];
|
||||||
prevRoomsRef.current.forEach((roomId) => {
|
prevRoomsRef.current.forEach((roomId) => {
|
||||||
if (!nextRooms.has(roomId)) changed.push(roomId);
|
if (!nextRooms.has(roomId)) removed.push(roomId);
|
||||||
});
|
});
|
||||||
nextRooms.forEach((roomId) => {
|
nextRooms.forEach((roomId) => {
|
||||||
if (!prevRoomsRef.current.has(roomId)) changed.push(roomId);
|
if (!prevRoomsRef.current.has(roomId)) added.push(roomId);
|
||||||
});
|
});
|
||||||
prevRoomsRef.current = nextRooms;
|
prevRoomsRef.current = nextRooms;
|
||||||
|
|
||||||
if (changed.length === 0) return;
|
const toDismiss = appActive ? [...removed, ...added] : removed;
|
||||||
if (!isNativePlatform()) return;
|
dismissRooms(toDismiss).catch(() => {
|
||||||
|
/* best-effort */
|
||||||
|
});
|
||||||
|
}, [incoming, appActive]);
|
||||||
|
|
||||||
(async () => {
|
// Handoff sweep on background → foreground. Reads prevRoomsRef populated by
|
||||||
const { PushNotifications } = await import('@capacitor/push-notifications');
|
// the diff-effect above. Also fires on initial mount when appActive=true
|
||||||
// Shape mirrors VojoFirebaseMessagingService.showIncomingCallNotification:
|
// with a non-empty atom (Answer-from-killed cold boot where /sync populated
|
||||||
// tag = "call_" + roomId, id = tag.hashCode(). The Android Capacitor
|
// the atom before first render) — JS now owns ring UX so any native
|
||||||
// plugin reads id via JSObject.getInteger (PushNotificationsPlugin.java
|
// CallStyle raised by the FCM path must go. Double-dismiss with the
|
||||||
// line 166), so the id must go over the bridge as a number — the TS
|
// diff-effect on a simultaneous transition+ADD tick is harmless (native
|
||||||
// type declares string, hence the cast.
|
// cancel is idempotent).
|
||||||
const notifications = changed.map((roomId) => {
|
useEffect(() => {
|
||||||
const tag = `call_${roomId}`;
|
if (!appActive) return;
|
||||||
let id = javaStringHashCode(tag);
|
if (prevRoomsRef.current.size === 0) return;
|
||||||
if (id === SUMMARY_NOTIFICATION_ID) id += 1;
|
dismissRooms(Array.from(prevRoomsRef.current)).catch(() => {
|
||||||
return { id, tag };
|
/* best-effort */
|
||||||
});
|
});
|
||||||
await PushNotifications.removeDeliveredNotifications({
|
}, [appActive]);
|
||||||
notifications: notifications as unknown as Parameters<
|
|
||||||
typeof PushNotifications.removeDeliveredNotifications
|
|
||||||
>[0]['notifications'],
|
|
||||||
}).catch(() => {
|
|
||||||
/* best-effort — alarm fallback still dismisses at lifetime expiry */
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}, [incoming]);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue