diff --git a/mobile_app/hooks/useMessageNotifications.ts b/mobile_app/hooks/useMessageNotifications.ts index 9b04de63..f107283d 100644 --- a/mobile_app/hooks/useMessageNotifications.ts +++ b/mobile_app/hooks/useMessageNotifications.ts @@ -82,8 +82,20 @@ export function useMessageNotifications( if (appStateRef.current === 'active') { onInApp(payload); } else { + // OS notification path (lock screen / Notification Center) shows no + // identifying content — generic title + body keep sender and message + // out of any screen the user hasn't unlocked into the app for. The + // in-app banner above still surfaces sender when the app is active. + // Per-peer identifier collapses repeats; passive interruption stops + // re-presenting the banner on every follow-up. Notifications.scheduleNotificationAsync({ - content: { title: sender, body: 'new message', sound: true }, + identifier: `anonmesh-msg-${srcHash}`, + content: { + title: 'anonmesh', + body: 'New message', + sound: true, + interruptionLevel: 'passive', + }, trigger: null, }).catch(() => {}); } diff --git a/mobile_app/hooks/usePeerCountNotification.ts b/mobile_app/hooks/usePeerCountNotification.ts index 6d5b6987..65e81023 100644 --- a/mobile_app/hooks/usePeerCountNotification.ts +++ b/mobile_app/hooks/usePeerCountNotification.ts @@ -1,4 +1,5 @@ import { useEffect, useRef } from 'react'; +import { Platform } from 'react-native'; import * as Notifications from 'expo-notifications'; import { useLxmfContext } from '@/context/LxmfContext'; @@ -6,25 +7,57 @@ const NOTIF_ID = 'anonmesh-peer-count'; export function usePeerCountNotification(enabled = true) { const { peers } = useLxmfContext(); - const prevOnlineRef = useRef(-1); - const prevTotalRef = useRef(-1); + const prevOnlineRef = useRef(-1); + const prevTotalRef = useRef(-1); + const prevHadPeersRef = useRef(null); useEffect(() => { if (!enabled) { Notifications.dismissNotificationAsync(NOTIF_ID).catch(() => {}); - prevOnlineRef.current = -1; - prevTotalRef.current = -1; + prevOnlineRef.current = -1; + prevTotalRef.current = -1; + prevHadPeersRef.current = null; return; } - const online = peers.filter(p => p.online).length; - const total = peers.length; + const online = peers.filter(p => p.online).length; + const total = peers.length; + const hasPeers = total > 0; + // iOS: schedule only on 0↔peers transition. Notification Center has no + // silent persistent surface — every reschedule resurfaces the entry. One + // quiet "mesh active" pill is enough; the in-app peer indicator already + // shows live counts. + if (Platform.OS === 'ios') { + if (prevHadPeersRef.current === hasPeers) return; + prevHadPeersRef.current = hasPeers; + + if (!hasPeers) { + Notifications.dismissNotificationAsync(NOTIF_ID).catch(() => {}); + return; + } + + Notifications.scheduleNotificationAsync({ + identifier: NOTIF_ID, + content: { + title: 'anonmesh', + body: 'Mesh active', + sound: false, + sticky: true, + interruptionLevel: 'passive', + }, + trigger: null, + }).catch(() => {}); + return; + } + + // Android: live counts on the LOW-importance 'peer-status' channel — the + // channel handles silence, so updates can flow without annoying the user. if (online === prevOnlineRef.current && total === prevTotalRef.current) return; prevOnlineRef.current = online; prevTotalRef.current = total; - if (total === 0) { + if (!hasPeers) { Notifications.dismissNotificationAsync(NOTIF_ID).catch(() => {}); return; }