A much more detailed notification
This commit is contained in:
+149
-17
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user