From 6062f91c2d8c7d389e1939760e8dd108d6299d34 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 10 May 2026 17:48:13 +0200 Subject: [PATCH] Throttle message notifications during sync --- lib/providers/app_provider.dart | 14 ++++-- lib/providers/messages_provider.dart | 19 ++++++- lib/services/notification_service.dart | 68 ++++++++++++++++++++++---- 3 files changed, 86 insertions(+), 15 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 916b39f..91729fc 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -2567,8 +2567,11 @@ class AppProvider with ChangeNotifier { debugPrint( '🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)', ); - final initialMessageCount = await connectionProvider.syncAllMessages( - force: true, + final initialMessageCount = + await messagesProvider.withReceivedNotificationsSuppressed( + () => connectionProvider.syncAllMessages( + force: true, + ), ); debugPrint( '📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)', @@ -2627,8 +2630,11 @@ class AppProvider with ChangeNotifier { ); await refreshChannelLocationSharingState(); - final messageCount = await connectionProvider.syncAllMessages( - force: true, + final messageCount = + await messagesProvider.withReceivedNotificationsSuppressed( + () => connectionProvider.syncAllMessages( + force: true, + ), ); debugPrint( '📥 [AppProvider] Reconnect sync retrieved $messageCount message(s)', diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index 3c0f48e..41cad02 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -36,6 +36,7 @@ class MessagesProvider with ChangeNotifier { bool _isInitialized = false; bool _isPersisting = false; bool _persistRequested = false; + bool _suppressReceivedNotifications = false; AppLocalizations? _localizations; final Map _messageContactLocations = {}; final Map _messageReceptionDetails = {}; @@ -850,11 +851,13 @@ class MessagesProvider with ChangeNotifier { _sarMarkers[marker.id] = marker; // Trigger urgent notification for received SAR messages (not sent by user) - if (!finalMessage.isSentMessage) { + if (!finalMessage.isSentMessage && !_suppressReceivedNotifications) { _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) _triggerMessageNotification(finalMessage); } @@ -865,6 +868,18 @@ class MessagesProvider with ChangeNotifier { notifyListeners(); } + Future withReceivedNotificationsSuppressed( + Future Function() action, + ) async { + final previous = _suppressReceivedNotifications; + _suppressReceivedNotifications = true; + try { + return await action(); + } finally { + _suppressReceivedNotifications = previous; + } + } + /// Check if a message is a duplicate /// /// Messages are considered duplicates if they have: diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 178ca9d..d190a04 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -29,6 +31,13 @@ class NotificationService { bool _muteForegroundNotifications = true; AppLifecycleState _lifecycleState = AppLifecycleState.resumed; String? _launchPayload; + Timer? _messageNotificationTimer; + int _pendingMessageNotificationCount = 0; + String? _pendingMessageSenderName; + String? _pendingMessageText; + bool _pendingMessageIsChannel = false; + String? _pendingMessageChannelName; + AppLocalizations? _pendingMessageLocalizations; // Notification IDs static const int _sarNotificationId = 1000; @@ -36,6 +45,8 @@ class NotificationService { static const int _updateNotificationId = 3000; static const int _batteryNotificationId = 4000; static const int _discoveryNotificationId = 5000; + static const Duration _messageNotificationCoalesceWindow = + Duration(seconds: 2); // Notification channels static const String _urgentChannelId = 'sar_urgent'; @@ -533,17 +544,53 @@ class NotificationService { return; } - try { - // Generate unique notification ID based on timestamp - final notificationId = - _messageNotificationId + - (DateTime.now().millisecondsSinceEpoch % 1000); + _pendingMessageNotificationCount++; + _pendingMessageSenderName = senderName; + _pendingMessageText = messageText; + _pendingMessageIsChannel = isChannelMessage; + _pendingMessageChannelName = channelName; + _pendingMessageLocalizations = localizations; + _messageNotificationTimer?.cancel(); + _messageNotificationTimer = Timer(_messageNotificationCoalesceWindow, () { + unawaited(_flushMessageNotification()); + }); + } + + Future _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 final resolvedChannelName = channelName?.trim().isNotEmpty == true ? channelName!.trim() : (localizations?.publicChannel ?? 'Public'); - final title = isChannelMessage + final title = count > 1 + ? (localizations?.messages ?? 'Messages') + : isChannelMessage ? (localizations != null ? '${localizations.channel}: $resolvedChannelName' : 'Channel: $resolvedChannelName') @@ -551,7 +598,9 @@ class NotificationService { ? '${localizations.newMessage} ${localizations.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; @@ -570,7 +619,7 @@ class NotificationService { styleInformation: BigTextStyleInformation( body, contentTitle: title, - summaryText: senderName, + summaryText: count > 1 ? body : senderName, ), ); @@ -594,7 +643,7 @@ class NotificationService { // Show notification await _notificationsPlugin.show( - id: notificationId, + id: _messageNotificationId, title: title, body: body, notificationDetails: notificationDetails, @@ -602,6 +651,7 @@ class NotificationService { ); debugPrint('✅ [NotificationService] Showed message notification'); + debugPrint(' Count: $count'); debugPrint(' Sender: $senderName'); debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}'); } catch (e) {