Throttle message notifications during sync

This commit is contained in:
Janez T
2026-05-10 17:48:13 +02:00
parent e1400cf1ec
commit 6062f91c2d
3 changed files with 86 additions and 15 deletions

View File

@@ -2567,8 +2567,11 @@ class AppProvider with ChangeNotifier {
debugPrint( debugPrint(
'🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)', '🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)',
); );
final initialMessageCount = await connectionProvider.syncAllMessages( final initialMessageCount =
await messagesProvider.withReceivedNotificationsSuppressed(
() => connectionProvider.syncAllMessages(
force: true, force: true,
),
); );
debugPrint( debugPrint(
'📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)', '📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)',
@@ -2627,8 +2630,11 @@ class AppProvider with ChangeNotifier {
); );
await refreshChannelLocationSharingState(); await refreshChannelLocationSharingState();
final messageCount = await connectionProvider.syncAllMessages( final messageCount =
await messagesProvider.withReceivedNotificationsSuppressed(
() => connectionProvider.syncAllMessages(
force: true, force: true,
),
); );
debugPrint( debugPrint(
'📥 [AppProvider] Reconnect sync retrieved $messageCount message(s)', '📥 [AppProvider] Reconnect sync retrieved $messageCount message(s)',

View File

@@ -36,6 +36,7 @@ class MessagesProvider with ChangeNotifier {
bool _isInitialized = false; bool _isInitialized = false;
bool _isPersisting = false; bool _isPersisting = false;
bool _persistRequested = false; bool _persistRequested = false;
bool _suppressReceivedNotifications = false;
AppLocalizations? _localizations; AppLocalizations? _localizations;
final Map<String, MessageContactLocation> _messageContactLocations = {}; final Map<String, MessageContactLocation> _messageContactLocations = {};
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {}; final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
@@ -850,11 +851,13 @@ class MessagesProvider with ChangeNotifier {
_sarMarkers[marker.id] = marker; _sarMarkers[marker.id] = marker;
// Trigger urgent notification for received SAR messages (not sent by user) // Trigger urgent notification for received SAR messages (not sent by user)
if (!finalMessage.isSentMessage) { if (!finalMessage.isSentMessage && !_suppressReceivedNotifications) {
_triggerSarNotification(finalMessage, marker); _triggerSarNotification(finalMessage, marker);
} }
} }
} else if (!finalMessage.isSentMessage && !finalMessage.isSystemMessage) { } else if (!finalMessage.isSentMessage &&
!finalMessage.isSystemMessage &&
!_suppressReceivedNotifications) {
// Trigger notification for regular messages (not SAR, not sent by user, not system) // Trigger notification for regular messages (not SAR, not sent by user, not system)
_triggerMessageNotification(finalMessage); _triggerMessageNotification(finalMessage);
} }
@@ -865,6 +868,18 @@ class MessagesProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<T> withReceivedNotificationsSuppressed<T>(
Future<T> Function() action,
) async {
final previous = _suppressReceivedNotifications;
_suppressReceivedNotifications = true;
try {
return await action();
} finally {
_suppressReceivedNotifications = previous;
}
}
/// Check if a message is a duplicate /// Check if a message is a duplicate
/// ///
/// Messages are considered duplicates if they have: /// Messages are considered duplicates if they have:

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -29,6 +31,13 @@ class NotificationService {
bool _muteForegroundNotifications = true; bool _muteForegroundNotifications = true;
AppLifecycleState _lifecycleState = AppLifecycleState.resumed; AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
String? _launchPayload; String? _launchPayload;
Timer? _messageNotificationTimer;
int _pendingMessageNotificationCount = 0;
String? _pendingMessageSenderName;
String? _pendingMessageText;
bool _pendingMessageIsChannel = false;
String? _pendingMessageChannelName;
AppLocalizations? _pendingMessageLocalizations;
// Notification IDs // Notification IDs
static const int _sarNotificationId = 1000; static const int _sarNotificationId = 1000;
@@ -36,6 +45,8 @@ class NotificationService {
static const int _updateNotificationId = 3000; static const int _updateNotificationId = 3000;
static const int _batteryNotificationId = 4000; static const int _batteryNotificationId = 4000;
static const int _discoveryNotificationId = 5000; static const int _discoveryNotificationId = 5000;
static const Duration _messageNotificationCoalesceWindow =
Duration(seconds: 2);
// Notification channels // Notification channels
static const String _urgentChannelId = 'sar_urgent'; static const String _urgentChannelId = 'sar_urgent';
@@ -533,17 +544,53 @@ class NotificationService {
return; return;
} }
try { _pendingMessageNotificationCount++;
// Generate unique notification ID based on timestamp _pendingMessageSenderName = senderName;
final notificationId = _pendingMessageText = messageText;
_messageNotificationId + _pendingMessageIsChannel = isChannelMessage;
(DateTime.now().millisecondsSinceEpoch % 1000); _pendingMessageChannelName = channelName;
_pendingMessageLocalizations = localizations;
_messageNotificationTimer?.cancel();
_messageNotificationTimer = Timer(_messageNotificationCoalesceWindow, () {
unawaited(_flushMessageNotification());
});
}
Future<void> _flushMessageNotification() async {
final count = _pendingMessageNotificationCount;
final senderName = _pendingMessageSenderName;
final messageText = _pendingMessageText;
final isChannelMessage = _pendingMessageIsChannel;
final channelName = _pendingMessageChannelName;
final localizations = _pendingMessageLocalizations;
_pendingMessageNotificationCount = 0;
_pendingMessageSenderName = null;
_pendingMessageText = null;
_pendingMessageIsChannel = false;
_pendingMessageChannelName = null;
_pendingMessageLocalizations = null;
if (count == 0 || senderName == null || messageText == null) {
return;
}
if (!_isInitialized ||
!_permissionGranted ||
!_messageNotificationsEnabled ||
_shouldSuppressForegroundNotifications()) {
return;
}
try {
// Build notification title and body // Build notification title and body
final resolvedChannelName = channelName?.trim().isNotEmpty == true final resolvedChannelName = channelName?.trim().isNotEmpty == true
? channelName!.trim() ? channelName!.trim()
: (localizations?.publicChannel ?? 'Public'); : (localizations?.publicChannel ?? 'Public');
final title = isChannelMessage final title = count > 1
? (localizations?.messages ?? 'Messages')
: isChannelMessage
? (localizations != null ? (localizations != null
? '${localizations.channel}: $resolvedChannelName' ? '${localizations.channel}: $resolvedChannelName'
: 'Channel: $resolvedChannelName') : 'Channel: $resolvedChannelName')
@@ -551,7 +598,9 @@ class NotificationService {
? '${localizations.newMessage} ${localizations.from} $senderName' ? '${localizations.newMessage} ${localizations.from} $senderName'
: 'New message from $senderName'); : 'New message from $senderName');
final body = messageText.length > 200 final body = count > 1
? 'You have $count new messages'
: messageText.length > 200
? '${messageText.substring(0, 200)}...' ? '${messageText.substring(0, 200)}...'
: messageText; : messageText;
@@ -570,7 +619,7 @@ class NotificationService {
styleInformation: BigTextStyleInformation( styleInformation: BigTextStyleInformation(
body, body,
contentTitle: title, contentTitle: title,
summaryText: senderName, summaryText: count > 1 ? body : senderName,
), ),
); );
@@ -594,7 +643,7 @@ class NotificationService {
// Show notification // Show notification
await _notificationsPlugin.show( await _notificationsPlugin.show(
id: notificationId, id: _messageNotificationId,
title: title, title: title,
body: body, body: body,
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
@@ -602,6 +651,7 @@ class NotificationService {
); );
debugPrint('✅ [NotificationService] Showed message notification'); debugPrint('✅ [NotificationService] Showed message notification');
debugPrint(' Count: $count');
debugPrint(' Sender: $senderName'); debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}'); debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
} catch (e) { } catch (e) {