import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'models/init_get_it.dart'; import 'models/contact_model.dart'; import 'models/user_model.dart'; import 'request/user_profile.dart'; import 'utils/app_dir.dart'; /// Here, the first argument is the id of notification and is common to all /// methods that would result in a notification being shown. This is typically /// set a unique value per notification as using the same id multiple times /// would result in a notification being updated/overwritten. class ReceivedNotification { final int id; final String? title; final String? body; final String? payload; const ReceivedNotification({ required this.id, required this.title, required this.body, required this.payload, }); } /// Before release build of app, read [Release build configuration] /// https://pub-web.flutter-io.cn/packages/flutter_local_notifications class NotificationAPI { static int id = 0; static Map)> messages = {}; /// Init notification plugin static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); /// Streams are created so that app can respond to notification-related events /// since the plugin is initialised in the `main` function /// Only be used in IOS? static final StreamController didReceiveNotificationStream = StreamController.broadcast(); static final StreamController selectNotificationStream = StreamController.broadcast(); static const MethodChannel platform = MethodChannel('dexterx.dev/flutter_local_notifications_example'); static const String portName = 'notification_send_port'; String? selectedNotificationPayload; static late final NotificationAppLaunchDetails? notificationAppLaunchDetails; /// A notification action which triggers a url launch event static const String urlLanunchActionId = 'id_1'; /// A notification action which triggers a App navigation event static const String navigationActionId = 'id-3'; /// Defines a iOS/MacOS notification category for text input actions. static const String darwinNotificationCategoryText = 'textCategory'; /// Defines a iOS/MacOS notification category for plain actions. static const String darwinNotificationCategoryPlain = 'plainCategory'; static Future init() async { WidgetsFlutterBinding.ensureInitialized(); await _configureLocalTimeZone(); // not null if platform is Linux notificationAppLaunchDetails = !kIsWeb && Platform.isLinux ? null // Use the getNotificationAppLaunchDetails method when the app starts // if you need to handle when a notification triggering the launch for // an app e.g. change the home route of the app for deep-linking. : await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); // ignore: unused_local_variable String initRouteName = 'Home'; if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { // If the app was launched via notification // (e.g. taps a notification out of the app). // route change depends on what type message is. Such as receiving a chat // message, this route should change to message screen, and receiving a apply // request shuold change to apply screen initRouteName = 'Message'; // TODO: Set the route according to message type } const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('app_icon'); final List darwinNotificationCategories = [ DarwinNotificationCategory( darwinNotificationCategoryText, actions: [ DarwinNotificationAction.text( 'text_1', 'action 1', buttonTitle: 'send', placeholder: 'Placeholder', ), ], ), DarwinNotificationCategory( darwinNotificationCategoryPlain, actions: [ DarwinNotificationAction.plain('id_1', 'Action 1'), DarwinNotificationAction.plain( 'id_2', 'Action 2 (destructive)', options: { DarwinNotificationActionOption.destructive, }, ), DarwinNotificationAction.plain( navigationActionId, 'Action 3 (foreground)', options: { DarwinNotificationActionOption.foreground, }, ), DarwinNotificationAction.plain( 'id_4', 'Action 4 (auth required)', options: { DarwinNotificationActionOption.authenticationRequired, }, ), ], options: { DarwinNotificationCategoryOption.hiddenPreviewShowTitle, }, ), ]; /// Note: permissions aren't requested here just to demonstrate that can be /// done later final DarwinInitializationSettings darwinInitializationSettings = DarwinInitializationSettings( requestAlertPermission: false, requestBadgePermission: false, requestSoundPermission: false, onDidReceiveLocalNotification: ( int id, String? title, String? body, String? payload, ) async { didReceiveNotificationStream.add( ReceivedNotification( id: id, title: title, body: body, payload: payload, ), ); }, notificationCategories: darwinNotificationCategories, ); final InitializationSettings initializationSettings = InitializationSettings( android: androidInitializationSettings, iOS: darwinInitializationSettings, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, // This callback fire when a notification has tapped onDidReceiveNotificationResponse: (NotificationResponse notificationRes) { switch (notificationRes.notificationResponseType) { case NotificationResponseType.selectedNotification: selectNotificationStream.add(notificationRes.payload); break; case NotificationResponseType.selectedNotificationAction: if (notificationRes.actionId == navigationActionId) { selectNotificationStream.add(notificationRes.payload); } break; } }, onDidReceiveBackgroundNotificationResponse: _notificationTapBackground, ); } @pragma('vm:entry-point') static void _notificationTapBackground( NotificationResponse notificationResponse) { print('notification(${notificationResponse.id}) action tapped: ' '${notificationResponse.actionId} with' ' payload: ${notificationResponse.payload}'); if (notificationResponse.input?.isNotEmpty ?? false) { print( 'notification action tapped with input: ${notificationResponse.input}'); } } /// Scheduling notifications now requires developers to specify a date and /// time relative to a specific time zone. /// This is to solve issues with daylight saving time that existed in the /// schedule method that is now deprecated. static Future _configureLocalTimeZone() async { if (kIsWeb || Platform.isLinux) { return; } tz.initializeTimeZones(); final String timeZoneName = await FlutterTimezone.getLocalTimezone(); tz.setLocalLocation(tz.getLocation(timeZoneName)); } static Future showMessageNotifications({ required String senderId, required String name, required String avatar, required String text, String? groupChatId, }) async { String groupChannelId = 'TOGETHER_${getIt.get().id}'; const String groupChannelName = 'together messages'; String avatarPath = ''; if (avatar.isNotEmpty) { if (groupChatId == null) { avatarPath = await getAvatarPath('user', avatar); if (!File(avatarPath).existsSync()) { File file = await File(avatarPath).create(recursive: true); final bytes = await downloadUserAvatar(avatar); await file.writeAsBytes(bytes); } } else { // TODO: download group chat avatar. This will done after changing group chat avatar is implemented } } late Person person; if (avatarPath.isNotEmpty) { person = Person( name: name, key: senderId, bot: false, icon: BitmapFilePathAndroidIcon(avatarPath), ); } else { person = Person( name: name, key: senderId, bot: false, ); } if (groupChatId == null) { if (messages.containsKey(senderId)) { messages[senderId]!.$2.add(Message(text, DateTime.now(), person)); } else { messages[senderId] = (++id, [Message(text, DateTime.now(), person)]); } } else { if (messages.containsKey(groupChatId)) { messages[groupChatId]!.$2.add(Message(text, DateTime.now(), person)); } else { messages[groupChatId] = (++id, [Message(text, DateTime.now(), person)]); } } late final MessagingStyleInformation messagingStyle; if (groupChatId == null) { messagingStyle = MessagingStyleInformation( person, messages: messages[senderId]!.$2, conversationTitle: name, ); } else { String groupChatName = getIt.get().groupChats[groupChatId]!.groupChatRemark.isEmpty ? getIt.get().groupChats[groupChatId]!.name : getIt.get().groupChats[groupChatId]!.groupChatRemark; messagingStyle = MessagingStyleInformation( person, messages: messages[groupChatId]!.$2, conversationTitle: groupChatName, groupConversation: true, ); } AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( groupChannelId, groupChannelName, importance: Importance.max, priority: Priority.high, category: AndroidNotificationCategory.message, styleInformation: messagingStyle, ); NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, ); late String payload; if (groupChatId == null) { payload = json.encode({ 'event': 'friend-message', 'friendId': senderId, }); await flutterLocalNotificationsPlugin.show( messages[senderId]!.$1, name, text, notificationDetails, payload: payload, ); } else { payload = json.encode({ 'event': 'group-chat-message', 'groupChatId': groupChatId, }); await flutterLocalNotificationsPlugin.show( messages[groupChatId]!.$1, name, text, notificationDetails, payload: payload, ); } } }