A much more detailed notification

This commit is contained in:
2026-02-27 07:05:08 +08:00
parent 9cc99e612a
commit dab43a7f30
9 changed files with 178 additions and 41 deletions
+149 -17
View File
@@ -18,6 +18,56 @@ import 'utils/notification_permission.dart';
import 'services/notification_service.dart';
import 'services/notification_bridge.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
final AudioPlayer _player = AudioPlayer();
Map<String, String> _formatNotificationFromData(Map<String, dynamic> data) {
final actor =
(data['actor_name'] ??
data['mentioner_name'] ??
data['user_name'] ??
data['from'] ??
'Someone')
.toString();
final taskId = data['task_id'] ?? data['taskId'] ?? data['task'] ?? '';
final ticketId =
data['ticket_id'] ?? data['ticketId'] ?? data['ticket'] ?? '';
final type = (data['type'] ?? '').toString().toLowerCase();
if (taskId.isNotEmpty &&
(type.contains('assign') ||
data['action'] == 'assign' ||
data['assigned'] == 'true')) {
return {
'title': 'Task assigned',
'body': '$actor has assigned you a Task #$taskId',
};
}
if (taskId.isNotEmpty &&
(type.contains('mention') ||
data['action'] == 'mention' ||
data['mentioned'] == 'true')) {
return {
'title': 'Mention',
'body': '$actor has mentioned you in Task #$taskId',
};
}
if (ticketId.isNotEmpty &&
(type.contains('mention') || data['action'] == 'mention')) {
return {
'title': 'Mention',
'body': '$actor has mentioned you in Ticket #$ticketId',
};
}
// Fallback to supplied title/body or generic
final title = data['title']?.toString() ?? 'New Notification';
final body = data['body']?.toString() ?? 'You have a new update in TasQ.';
return {'title': title, 'body': body};
}
// Initialize the plugin
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
@@ -30,16 +80,44 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
final FlutterLocalNotificationsPlugin localNotifPlugin =
FlutterLocalNotificationsPlugin();
// 2. Extract title and body from the DATA payload (not message.notification)
final String title = message.data['title'] ?? 'New Notification';
final String body = message.data['body'] ?? 'You have a new update in TasQ.';
// 2. Extract and format title/body from the DATA payload (not message.notification)
final formatted = _formatNotificationFromData(message.data);
final String title = formatted['title']!;
final String body = formatted['body']!;
// Create a unique ID
// Determine a stable ID for deduplication (prefer server-provided id)
final String stableId =
message.data['notification_id'] ??
message.messageId ??
(DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
// Dedupe: keep a short-lived cache of recent notification IDs to avoid duplicates
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString('recent_notifs') ?? '{}';
final Map<String, dynamic> recent = jsonDecode(raw);
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
const int ttl = 60; // seconds
// prune old entries
recent.removeWhere(
(k, v) => (v is int ? v : int.parse(v.toString())) < now - ttl,
);
if (recent.containsKey(stableId)) {
// already shown recently — skip
return;
}
recent[stableId] = now;
await prefs.setString('recent_notifs', jsonEncode(recent));
} catch (e) {
// If prefs fail in background isolate, fall back to showing notification
}
// Create a unique ID for the notification display
final int id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// 3. Define the exact same channel specifics
const androidDetails = AndroidNotificationDetails(
'tasq_custom_sound_channel_2',
'tasq_custom_sound_channel_3',
'High Importance Notifications',
importance: Importance.max,
priority: Priority.high,
@@ -125,13 +203,38 @@ Future<void> main() async {
await FirebaseMessaging.instance.requestPermission();
}
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final notification = message.notification;
if (notification != null) {
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
// Prefer the data payload and format friendly messages, with dedupe.
final formatted = _formatNotificationFromData(message.data);
final String stableId =
message.data['notification_id'] ??
message.messageId ??
(DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString('recent_notifs') ?? '{}';
final Map<String, dynamic> recent = jsonDecode(raw);
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
const int ttl = 60;
recent.removeWhere(
(k, v) => (v is int ? v : int.parse(v.toString())) < now - ttl,
);
if (!recent.containsKey(stableId)) {
recent[stableId] = now;
await prefs.setString('recent_notifs', jsonEncode(recent));
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: formatted['title']!,
body: formatted['body']!,
payload: message.data['payload'],
);
}
} catch (e) {
// On failure, just show the notification to avoid dropping alerts
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: notification.title ?? 'Notification',
body: notification.body ?? '',
title: formatted['title']!,
body: formatted['body']!,
payload: message.data['payload'],
);
}
@@ -202,7 +305,6 @@ Future<void> main() async {
}
class NotificationSoundObserver extends ProviderObserver {
static final AudioPlayer _player = AudioPlayer();
StreamSubscription<String?>? _tokenSub;
@override
@@ -217,12 +319,7 @@ class NotificationSoundObserver extends ProviderObserver {
final prev = previousValue as int?;
final next = newValue as int?;
if (prev != null && next != null && next > prev) {
_player.play(AssetSource('tasq_notification.wav'));
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: 'New notifications',
body: 'You have $next unread notifications.',
);
_maybeShowUnreadNotification(next);
}
}
@@ -273,6 +370,41 @@ class NotificationSoundObserver extends ProviderObserver {
}
}
void _maybeShowUnreadNotification(int next) async {
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString('recent_notifs') ?? '{}';
final Map<String, dynamic> recent = jsonDecode(raw);
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
const int ttl = 60; // seconds
// prune old entries
recent.removeWhere(
(k, v) => (v is int ? v : int.parse(v.toString())) < now - ttl,
);
// if there is any recent notification (from server or other), skip duplicating
if (recent.isNotEmpty) return;
// mark a synthetic unread-notif to prevent immediate duplicates
recent['unread_summary'] = now;
await prefs.setString('recent_notifs', jsonEncode(recent));
_player.play(AssetSource('tasq_notification.wav'));
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: 'New notifications',
body: 'You have $next unread notifications.',
);
} catch (e) {
// fallback: show notification
_player.play(AssetSource('tasq_notification.wav'));
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: 'New notifications',
body: 'You have $next unread notifications.',
);
}
}
class _MissingConfigApp extends StatelessWidget {
const _MissingConfigApp();