2023-06-21 17:44:28 +08:00
|
|
|
import 'dart:async';
|
2023-09-10 11:30:20 +08:00
|
|
|
import 'dart:convert';
|
2023-09-09 16:48:47 +08:00
|
|
|
import 'dart:io';
|
2023-06-21 17:44:28 +08:00
|
|
|
|
2023-09-09 16:48:47 +08:00
|
|
|
import 'package:flutter/cupertino.dart';
|
|
|
|
import 'package:flutter/foundation.dart';
|
2023-06-21 17:44:28 +08:00
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
2023-09-09 16:48:47 +08:00
|
|
|
import 'package:flutter_timezone/flutter_timezone.dart';
|
2023-06-21 17:44:28 +08:00
|
|
|
|
2023-09-09 16:48:47 +08:00
|
|
|
import 'package:timezone/data/latest_all.dart' as tz;
|
|
|
|
import 'package:timezone/timezone.dart' as tz;
|
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
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';
|
2023-06-21 17:44:28 +08:00
|
|
|
|
2023-09-09 16:48:47 +08:00
|
|
|
/// 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.
|
2023-06-21 17:44:28 +08:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
/// Before release build of app, read [Release build configuration]
|
|
|
|
/// https://pub-web.flutter-io.cn/packages/flutter_local_notifications
|
2023-06-21 17:44:28 +08:00
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
class NotificationAPI {
|
|
|
|
static int id = 0;
|
|
|
|
|
|
|
|
static Map<String, (int, List<Message>)> 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<ReceivedNotification?>
|
|
|
|
didReceiveNotificationStream =
|
|
|
|
StreamController<ReceivedNotification>.broadcast();
|
|
|
|
|
|
|
|
static final StreamController<String?> selectNotificationStream =
|
|
|
|
StreamController<String?>.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<void> 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();
|
|
|
|
|
2024-04-10 17:15:05 +08:00
|
|
|
// ignore: unused_local_variable
|
2023-09-10 11:30:20 +08:00
|
|
|
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<DarwinNotificationCategory> darwinNotificationCategories =
|
|
|
|
<DarwinNotificationCategory>[
|
|
|
|
DarwinNotificationCategory(
|
|
|
|
darwinNotificationCategoryText,
|
|
|
|
actions: <DarwinNotificationAction>[
|
|
|
|
DarwinNotificationAction.text(
|
|
|
|
'text_1',
|
|
|
|
'action 1',
|
|
|
|
buttonTitle: 'send',
|
|
|
|
placeholder: 'Placeholder',
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
DarwinNotificationCategory(
|
|
|
|
darwinNotificationCategoryPlain,
|
|
|
|
actions: <DarwinNotificationAction>[
|
|
|
|
DarwinNotificationAction.plain('id_1', 'Action 1'),
|
|
|
|
DarwinNotificationAction.plain(
|
|
|
|
'id_2',
|
|
|
|
'Action 2 (destructive)',
|
|
|
|
options: <DarwinNotificationActionOption>{
|
|
|
|
DarwinNotificationActionOption.destructive,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
DarwinNotificationAction.plain(
|
|
|
|
navigationActionId,
|
|
|
|
'Action 3 (foreground)',
|
|
|
|
options: <DarwinNotificationActionOption>{
|
|
|
|
DarwinNotificationActionOption.foreground,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
DarwinNotificationAction.plain(
|
|
|
|
'id_4',
|
|
|
|
'Action 4 (auth required)',
|
|
|
|
options: <DarwinNotificationActionOption>{
|
|
|
|
DarwinNotificationActionOption.authenticationRequired,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
options: <DarwinNotificationCategoryOption>{
|
|
|
|
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,
|
|
|
|
);
|
2023-09-09 16:48:47 +08:00
|
|
|
}
|
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
@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}');
|
|
|
|
}
|
2023-06-21 17:44:28 +08:00
|
|
|
}
|
2023-09-09 16:48:47 +08:00
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
/// 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<void> _configureLocalTimeZone() async {
|
|
|
|
if (kIsWeb || Platform.isLinux) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
tz.initializeTimeZones();
|
|
|
|
final String timeZoneName = await FlutterTimezone.getLocalTimezone();
|
|
|
|
tz.setLocalLocation(tz.getLocation(timeZoneName));
|
|
|
|
}
|
2023-09-09 16:48:47 +08:00
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
static Future<void> showMessageNotifications({
|
|
|
|
required String senderId,
|
|
|
|
required String name,
|
|
|
|
required String avatar,
|
|
|
|
required String text,
|
|
|
|
String? groupChatId,
|
|
|
|
}) async {
|
|
|
|
String groupChannelId = 'TOGETHER_${getIt.get<UserAccount>().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
|
|
|
|
}
|
|
|
|
}
|
2023-06-21 17:44:28 +08:00
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
late Person person;
|
2023-09-09 16:48:47 +08:00
|
|
|
|
2023-09-10 11:30:20 +08:00
|
|
|
if (avatarPath.isNotEmpty) {
|
|
|
|
person = Person(
|
|
|
|
name: name,
|
|
|
|
key: senderId,
|
|
|
|
bot: false,
|
|
|
|
icon: BitmapFilePathAndroidIcon(avatarPath),
|
2023-09-09 16:48:47 +08:00
|
|
|
);
|
2023-09-10 11:30:20 +08:00
|
|
|
} 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)]);
|
2023-09-09 16:48:47 +08:00
|
|
|
}
|
2023-09-10 11:30:20 +08:00
|
|
|
} 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 =
|
2023-09-18 19:32:04 +08:00
|
|
|
getIt.get<Contact>().groupChats[groupChatId]!.groupChatRemark.isEmpty
|
2023-09-10 11:30:20 +08:00
|
|
|
? getIt.get<ContactAccountProfile>().groupChats[groupChatId]!.name
|
2023-09-18 19:32:04 +08:00
|
|
|
: getIt.get<Contact>().groupChats[groupChatId]!.groupChatRemark;
|
2023-09-10 11:30:20 +08:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2023-09-09 16:48:47 +08:00
|
|
|
}
|