import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; /// Shows an OS notification when a private message arrives while the app is in /// the foreground. Foreground only by design — when the app is backgrounded the /// message listener stops, so nothing fires (background/push is a later, larger /// concern). Web and Windows have no local-notification support, so there this /// whole service is an inert no-op. /// /// For privacy it never carries message text — the caller passes a generic /// `New message from ` title (see [InboxService]). The peer's pubkey rides /// along as the payload so a tap can open that exact chat via [onTapChat]. class NotificationService { NotificationService({ FlutterLocalNotificationsPlugin? plugin, bool? supported, }) : _plugin = plugin ?? FlutterLocalNotificationsPlugin(), _supported = supported ?? _platformSupported; final FlutterLocalNotificationsPlugin _plugin; final bool _supported; bool _ready = false; /// Called with a peer pubkey when the user taps a message notification. The /// app wires this to `router.push('/chat/')` once the router exists. void Function(String peerPubkey)? onTapChat; static const _channelId = 'messages'; static bool get _platformSupported { if (kIsWeb) return false; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: case TargetPlatform.macOS: case TargetPlatform.linux: return true; case TargetPlatform.windows: case TargetPlatform.fuchsia: return false; } } /// Initialises the plugin and requests permission (Android 13+/iOS/macOS ask /// at runtime). Idempotent and safe to call on any platform. Future initialize() async { if (!_supported || _ready) return; const android = AndroidInitializationSettings('@mipmap/ic_launcher'); const darwin = DarwinInitializationSettings(); const linux = LinuxInitializationSettings(defaultActionName: 'Open'); await _plugin.initialize( const InitializationSettings( android: android, iOS: darwin, macOS: darwin, linux: linux, ), onDidReceiveNotificationResponse: (response) { final payload = response.payload; if (payload != null && payload.isNotEmpty) onTapChat?.call(payload); }, ); await _plugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.requestNotificationsPermission(); await _plugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions(alert: true, badge: true, sound: true); await _plugin .resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.requestPermissions(alert: true, badge: true, sound: true); _ready = true; } /// Shows a notification for a new message. [title] is a generic, text-free /// line like "New message from Alice"; [peerPubkey] becomes the tap payload. /// No-op on unsupported platforms. Future showMessage({ required String peerPubkey, required String title, }) async { if (!_supported) return; const details = NotificationDetails( android: AndroidNotificationDetails( _channelId, 'Messages', channelDescription: 'New private messages', importance: Importance.high, priority: Priority.high, ), iOS: DarwinNotificationDetails(), macOS: DarwinNotificationDetails(), linux: LinuxNotificationDetails(), ); // One notification per peer (same id replaces the previous), so a chatty // peer doesn't stack a wall of notifications. await _plugin.show(peerPubkey.hashCode, title, null, details, payload: peerPubkey); } }