✨ System notification for desktop and android
This commit is contained in:
		| @@ -6,6 +6,7 @@ import 'package:dio/dio.dart'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/main.dart'; | ||||
| @@ -16,54 +17,159 @@ import 'package:island/widgets/app_notification.dart'; | ||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = | ||||
|     FlutterLocalNotificationsPlugin(); | ||||
|  | ||||
| AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||
|  | ||||
| void _onAppLifecycleChanged(AppLifecycleState state) { | ||||
|   _appLifecycleState = state; | ||||
| } | ||||
|  | ||||
| Future<void> initializeLocalNotifications() async { | ||||
|   const AndroidInitializationSettings initializationSettingsAndroid = | ||||
|       AndroidInitializationSettings('@mipmap/ic_launcher'); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsIOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsMacOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const LinuxInitializationSettings initializationSettingsLinux = | ||||
|       LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|  | ||||
|   const WindowsInitializationSettings initializationSettingsWindows = | ||||
|       WindowsInitializationSettings( | ||||
|         appName: 'Island', | ||||
|         appUserModelId: 'dev.solsynth.solian', | ||||
|         guid: 'dev.solsynth.solian', | ||||
|       ); | ||||
|  | ||||
|   const InitializationSettings initializationSettings = InitializationSettings( | ||||
|     android: initializationSettingsAndroid, | ||||
|     iOS: initializationSettingsIOS, | ||||
|     macOS: initializationSettingsMacOS, | ||||
|     linux: initializationSettingsLinux, | ||||
|     windows: initializationSettingsWindows, | ||||
|   ); | ||||
|  | ||||
|   await flutterLocalNotificationsPlugin.initialize( | ||||
|     initializationSettings, | ||||
|     onDidReceiveNotificationResponse: (NotificationResponse response) async { | ||||
|       final payload = response.payload; | ||||
|       if (payload != null) { | ||||
|         if (payload.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           rootNavigatorKey.currentContext?.push(payload); | ||||
|         } else { | ||||
|           // External URLs | ||||
|           launchUrlString(payload); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   WidgetsBinding.instance.addObserver( | ||||
|     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class LifecycleEventHandler extends WidgetsBindingObserver { | ||||
|   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||
|  | ||||
|   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     onAppLifecycleChanged(state); | ||||
|   } | ||||
| } | ||||
|  | ||||
| StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
|   BuildContext context, | ||||
|   WidgetRef ref, | ||||
| ) { | ||||
|   final ws = ref.watch(websocketProvider); | ||||
|   return ws.dataStream.listen((pkt) { | ||||
|   return ws.dataStream.listen((pkt) async { | ||||
|     if (pkt.type == "notifications.new") { | ||||
|       final notification = SnNotification.fromJson(pkt.data!); | ||||
|       showTopSnackBar( | ||||
|         globalOverlay.currentState!, | ||||
|         Center( | ||||
|           child: ConstrainedBox( | ||||
|             constraints: const BoxConstraints(maxWidth: 480), | ||||
|             child: NotificationCard(notification: notification), | ||||
|       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||
|         // App is focused, show in-app notification | ||||
|         log( | ||||
|           '[Notification] Showing in-app notification: ${notification.title}', | ||||
|         ); | ||||
|         showTopSnackBar( | ||||
|           globalOverlay.currentState!, | ||||
|           Center( | ||||
|             child: ConstrainedBox( | ||||
|               constraints: const BoxConstraints(maxWidth: 480), | ||||
|               child: NotificationCard(notification: notification), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         onTap: () { | ||||
|           if (notification.meta['action_uri'] != null) { | ||||
|             var uri = notification.meta['action_uri'] as String; | ||||
|             if (uri.startsWith('/')) { | ||||
|               // In-app routes | ||||
|               rootNavigatorKey.currentContext?.push( | ||||
|                 notification.meta['action_uri'], | ||||
|               ); | ||||
|             } else { | ||||
|               // External URLs | ||||
|               launchUrlString(uri); | ||||
|           onTap: () { | ||||
|             if (notification.meta['action_uri'] != null) { | ||||
|               var uri = notification.meta['action_uri'] as String; | ||||
|               if (uri.startsWith('/')) { | ||||
|                 // In-app routes | ||||
|                 rootNavigatorKey.currentContext?.push( | ||||
|                   notification.meta['action_uri'], | ||||
|                 ); | ||||
|               } else { | ||||
|                 // External URLs | ||||
|                 launchUrlString(uri); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         onDismissed: () {}, | ||||
|         dismissType: DismissType.onSwipe, | ||||
|         displayDuration: const Duration(seconds: 5), | ||||
|         snackBarPosition: SnackBarPosition.top, | ||||
|         padding: EdgeInsets.only( | ||||
|           left: 16, | ||||
|           right: 16, | ||||
|           top: | ||||
|               (!kIsWeb && | ||||
|                       (Platform.isMacOS || | ||||
|                           Platform.isWindows || | ||||
|                           Platform.isLinux)) | ||||
|                   ? 28 | ||||
|                   // ignore: use_build_context_synchronously | ||||
|                   : MediaQuery.of(context).padding.top + 16, | ||||
|           bottom: 16, | ||||
|         ), | ||||
|       ); | ||||
|           }, | ||||
|           onDismissed: () {}, | ||||
|           dismissType: DismissType.onSwipe, | ||||
|           displayDuration: const Duration(seconds: 5), | ||||
|           snackBarPosition: SnackBarPosition.top, | ||||
|           padding: EdgeInsets.only( | ||||
|             left: 16, | ||||
|             right: 16, | ||||
|             top: | ||||
|                 (!kIsWeb && | ||||
|                         (Platform.isMacOS || | ||||
|                             Platform.isWindows || | ||||
|                             Platform.isLinux)) | ||||
|                     ? 28 | ||||
|                     // ignore: use_build_context_synchronously | ||||
|                     : MediaQuery.of(context).padding.top + 16, | ||||
|             bottom: 16, | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         // App is in background, show system notification (only on supported platforms) | ||||
|         if (!kIsWeb && !Platform.isIOS) { | ||||
|           log( | ||||
|             '[Notification] Showing system notification: ${notification.title}', | ||||
|           ); | ||||
|           const AndroidNotificationDetails androidNotificationDetails = | ||||
|               AndroidNotificationDetails( | ||||
|                 'channel_id', | ||||
|                 'channel_name', | ||||
|                 channelDescription: 'channel_description', | ||||
|                 importance: Importance.max, | ||||
|                 priority: Priority.high, | ||||
|                 ticker: 'ticker', | ||||
|               ); | ||||
|           const NotificationDetails notificationDetails = NotificationDetails( | ||||
|             android: androidNotificationDetails, | ||||
|           ); | ||||
|           await flutterLocalNotificationsPlugin.show( | ||||
|             0, | ||||
|             notification.title, | ||||
|             notification.content, | ||||
|             notificationDetails, | ||||
|             payload: notification.meta['action_uri'] as String?, | ||||
|           ); | ||||
|         } else { | ||||
|           log( | ||||
|             '[Notification] Skipping system notification for unsupported platform: ${notification.title}', | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| @@ -72,7 +178,7 @@ Future<void> subscribePushNotification( | ||||
|   Dio apiClient, { | ||||
|   bool detailedErrors = false, | ||||
| }) async { | ||||
|   if (Platform.isLinux) { | ||||
|   if (!kIsWeb && Platform.isLinux) { | ||||
|     return; | ||||
|   } | ||||
|   await FirebaseMessaging.instance.requestPermission( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user