Gate incoming-call ring audio on App.isActive to stop double-ring in the brief window after app backgrounding
This commit is contained in:
parent
e8188d7361
commit
2836411830
1 changed files with 49 additions and 6 deletions
|
|
@ -6,17 +6,24 @@
|
|||
// IncomingCallStripRenderer is platform-agnostic: if JS knows about an incoming
|
||||
// ring, we render the in-app strip. On Android the native FCM service decides
|
||||
// independently whether to surface a system CallStyle notification; when the
|
||||
// app is foregrounded it now suppresses that banner, so this renderer no longer
|
||||
// needs to mirror native foreground policy in JS.
|
||||
// app is foregrounded it suppresses that banner, so strip render itself does
|
||||
// not need to mirror foreground policy in JS.
|
||||
//
|
||||
// Ring audio DOES mirror foreground policy — gated on `appActive` (techdebt
|
||||
// §5.39): when the app/tab is hidden the platform surface (Android CallStyle /
|
||||
// web SW push) owns ringtone UX, and playing the in-app <audio> on top would
|
||||
// double-ring during the grace window after backgrounding while the WebView
|
||||
// still processes /sync.
|
||||
//
|
||||
// KNOWN GAP §5.17: if the browser blocks `audio.play()` (cold page load, no
|
||||
// user gesture yet), the ring is silent — strip is still visible but user may
|
||||
// miss it. Fallback (click-to-enable, pulsing animation, Web Notifications) is
|
||||
// Phase 3 polish.
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Box } from 'folds';
|
||||
import { App } from '@capacitor/app';
|
||||
import { incomingCallsAtom } from '../state/incomingCalls';
|
||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||
import { IncomingCallStrip } from '../features/call-status';
|
||||
|
|
@ -29,22 +36,58 @@ export function IncomingCallStripRenderer() {
|
|||
const mx = useMatrixClient();
|
||||
const incoming = useAtomValue(incomingCallsAtom);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const [appActive, setAppActive] = useState(
|
||||
() => typeof document === 'undefined' || document.visibilityState === 'visible'
|
||||
);
|
||||
|
||||
const hasIncoming = incoming.size > 0;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
// Listener registration and getState() race: if a real appStateChange
|
||||
// fires before getState() resolves, the stale getState() snapshot would
|
||||
// overwrite the fresh event and re-open §5.39 (e.g. background → listener
|
||||
// sets false → late getState() resolves true → audio re-enabled).
|
||||
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?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
if (hasIncoming) {
|
||||
if (hasIncoming && appActive) {
|
||||
audio.currentTime = 0;
|
||||
audio.play().catch(() => {
|
||||
// autoplay blocked — strip UI still visible
|
||||
});
|
||||
} else {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
}, [hasIncoming]);
|
||||
}, [hasIncoming, appActive]);
|
||||
|
||||
const entries = Array.from(incoming.values());
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue