Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/app/hooks/useAppVisibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useClientConfig } from './useClientConfig';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { pushSubscriptionAtom } from '../state/pushSubscription';
import { mobileOrTablet } from '../utils/user-agent';
import { createDebugLogger } from '../utils/debugLogger';

const debugLog = createDebugLogger('AppVisibility');
Expand All @@ -16,7 +15,6 @@ export function useAppVisibility(mx: MatrixClient | undefined) {
const clientConfig = useClientConfig();
const [usePushNotifications] = useSetting(settingsAtom, 'usePushNotifications');
const pushSubAtom = useAtom(pushSubscriptionAtom);
const isMobile = mobileOrTablet();

useEffect(() => {
const handleVisibilityChange = () => {
Expand All @@ -43,12 +41,17 @@ export function useAppVisibility(mx: MatrixClient | undefined) {
if (!mx) return undefined;

const handleVisibilityForNotifications = (isVisible: boolean) => {
togglePusher(mx, clientConfig, isVisible, usePushNotifications, pushSubAtom, isMobile);
// Always keep the pusher registered regardless of visibility — the SW's
// hasVisibleClient check handles OS-notification suppression when the app
// is in the foreground, so we never need to delete the pusher. Keeping
// it permanently avoids the enable/disable race that can leave the
// homeserver without a valid pusher after rapid tab-focus changes.
togglePusher(mx, clientConfig, isVisible, usePushNotifications, pushSubAtom, true);
};

appEvents.onVisibilityChange = handleVisibilityForNotifications;
return () => {
appEvents.onVisibilityChange = null;
};
}, [mx, clientConfig, usePushNotifications, pushSubAtom, isMobile]);
}, [mx, clientConfig, usePushNotifications, pushSubAtom]);
}
17 changes: 14 additions & 3 deletions src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,10 +763,21 @@ const onPushNotification = async (event: PushEvent) => {

// If the app is open and visible, skip the OS push notification — the in-app
// pill notification handles the alert instead.
// Combine clients.matchAll() visibility with the explicit appIsVisible flag
// because iOS Safari PWA often returns empty or stale results from matchAll().
//
// Require BOTH the explicit appIsVisible flag AND a visible client from
// matchAll() before suppressing. appIsVisible resets to false every time the
// SW starts fresh; on iOS the browser kills the SW between pushes, so on the
// next push appIsVisible is always false — we never suppress on a cold SW
// restart, which prevents the "notifications stop after a while" bug where
// stale matchAll() data (visibilityState stuck at 'visible') would cause all
// subsequent notifications to be silently dropped.
//
// When matchAll() returns zero clients (iOS Safari PWA fully-suspended quirk),
// clients.some() returns false — do NOT suppress. Better to show a duplicate
// (handled gracefully by the in-app banner) than to silently drop a
// notification while the app is backgrounded.
const hasVisibleClient =
appIsVisible || clients.some((client) => client.visibilityState === 'visible');
appIsVisible && clients.some((client) => client.visibilityState === 'visible');
console.debug(
'[SW push] appIsVisible:',
appIsVisible,
Expand Down
Loading