mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
Fix RX advert data parsing
This commit is contained in:
@@ -17,6 +17,7 @@ import '../services/nearest_router_selector.dart';
|
||||
import '../services/packet_capture_storage_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/ble_packet_log.dart';
|
||||
@@ -59,6 +60,9 @@ class _DirectMessageRouteSession {
|
||||
/// Main App Provider - coordinates all other providers
|
||||
class AppProvider with ChangeNotifier {
|
||||
static const int _maxDirectPayloadHops = 3;
|
||||
static const double _lowBatteryThresholdPercent = 30.0;
|
||||
static const double _lowBatteryResetThresholdPercent = 35.0;
|
||||
static const Duration _lowBatteryCheckInterval = Duration(minutes: 5);
|
||||
@visibleForTesting
|
||||
static bool isDeletedChannelInfo(
|
||||
int channelIdx,
|
||||
@@ -88,6 +92,7 @@ class AppProvider with ChangeNotifier {
|
||||
LocationTrackingService();
|
||||
final PacketCaptureStorageService packetCaptureStorageService =
|
||||
PacketCaptureStorageService();
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
|
||||
bool _isInitialized = false;
|
||||
bool get isInitialized => _isInitialized;
|
||||
@@ -117,8 +122,6 @@ class AppProvider with ChangeNotifier {
|
||||
bool get isVoiceNoiseSuppressionEnabled => _isVoiceNoiseSuppressionEnabled;
|
||||
double _messageFontScale = 1.0;
|
||||
double get messageFontScale => _messageFontScale;
|
||||
bool _autoAddDiscoveredContacts = false;
|
||||
bool get autoAddDiscoveredContacts => _autoAddDiscoveredContacts;
|
||||
bool _autoRouteRotationEnabled =
|
||||
MessagingRoutePreferences.defaultAutoRouteRotationEnabled;
|
||||
bool get autoRouteRotationEnabled => _autoRouteRotationEnabled;
|
||||
@@ -150,11 +153,13 @@ class AppProvider with ChangeNotifier {
|
||||
_pendingMediaSwarmResponses = {};
|
||||
bool _fastLocationScreenActive = false;
|
||||
Timer? _packetCaptureFlushTimer;
|
||||
Timer? _lowBatteryCheckTimer;
|
||||
String? _lastPersistedPacketSignature;
|
||||
bool _isPersistingPacketCapture = false;
|
||||
bool _wasDeviceConnected = false;
|
||||
bool _hasCompletedConnectionBootstrap = false;
|
||||
bool _isReconnectSyncInProgress = false;
|
||||
final Set<String> _lowBatteryNotifiedNodeIds = <String>{};
|
||||
|
||||
AppProvider({
|
||||
required this.connectionProvider,
|
||||
@@ -181,10 +186,10 @@ class AppProvider with ChangeNotifier {
|
||||
_loadVoiceEchoCancellationEnabled();
|
||||
_loadVoiceNoiseSuppressionEnabled();
|
||||
_loadMessageFontScale();
|
||||
_loadAutoAddDiscoveredContacts();
|
||||
_loadMessagingRouteSettings();
|
||||
unawaited(_pathHistoryService.initialize());
|
||||
_startPacketCapturePersistence();
|
||||
_startLowBatteryWatcher();
|
||||
_syncDrawingsOnStartup(); // Sync drawings immediately after providers load
|
||||
_isInitialized = true;
|
||||
}
|
||||
@@ -197,6 +202,86 @@ class AppProvider with ChangeNotifier {
|
||||
unawaited(_flushPacketCaptureLogs());
|
||||
}
|
||||
|
||||
void _startLowBatteryWatcher() {
|
||||
_lowBatteryCheckTimer?.cancel();
|
||||
_lowBatteryCheckTimer = Timer.periodic(_lowBatteryCheckInterval, (_) {
|
||||
unawaited(_checkLowBatteryAlerts());
|
||||
});
|
||||
unawaited(_checkLowBatteryAlerts());
|
||||
}
|
||||
|
||||
Future<void> _checkLowBatteryAlerts() async {
|
||||
final recoveredIds = <String>{};
|
||||
|
||||
final deviceBattery = connectionProvider.deviceInfo.batteryPercent;
|
||||
if (deviceBattery != null &&
|
||||
deviceBattery > _lowBatteryResetThresholdPercent) {
|
||||
recoveredIds.add('device');
|
||||
}
|
||||
|
||||
for (final contact in contactsProvider.contacts) {
|
||||
if (contact.isChannel) continue;
|
||||
final battery = contact.displayBattery;
|
||||
if (battery == null) continue;
|
||||
if (battery > _lowBatteryResetThresholdPercent) {
|
||||
recoveredIds.add(contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
|
||||
if (recoveredIds.isNotEmpty) {
|
||||
_lowBatteryNotifiedNodeIds.removeAll(recoveredIds);
|
||||
}
|
||||
|
||||
if (connectionProvider.deviceInfo.isConnected && deviceBattery != null) {
|
||||
await _notifyLowBatteryIfNeeded(
|
||||
nodeId: 'device',
|
||||
nodeName:
|
||||
connectionProvider.deviceInfo.selfName?.trim().isNotEmpty == true
|
||||
? connectionProvider.deviceInfo.selfName!.trim()
|
||||
: (connectionProvider.deviceInfo.displayName ?? 'Connected device'),
|
||||
batteryPercent: deviceBattery,
|
||||
isCurrentDevice: true,
|
||||
);
|
||||
}
|
||||
|
||||
for (final contact in contactsProvider.contacts) {
|
||||
if (contact.isChannel) continue;
|
||||
final battery = contact.displayBattery;
|
||||
if (battery == null) continue;
|
||||
|
||||
await _notifyLowBatteryIfNeeded(
|
||||
nodeId: contact.publicKeyHex,
|
||||
nodeName: contact.displayName,
|
||||
batteryPercent: battery,
|
||||
isCurrentDevice: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _notifyLowBatteryIfNeeded({
|
||||
required String nodeId,
|
||||
required String nodeName,
|
||||
required double batteryPercent,
|
||||
required bool isCurrentDevice,
|
||||
}) async {
|
||||
if (batteryPercent >= _lowBatteryThresholdPercent) {
|
||||
return;
|
||||
}
|
||||
if (_lowBatteryNotifiedNodeIds.contains(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final shown = await _notificationService.showLowBatteryNotification(
|
||||
nodeId: nodeId,
|
||||
nodeName: nodeName,
|
||||
batteryPercent: batteryPercent,
|
||||
isCurrentDevice: isCurrentDevice,
|
||||
);
|
||||
if (shown) {
|
||||
_lowBatteryNotifiedNodeIds.add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
String _packetLogSignature(BlePacketLog log) {
|
||||
final prefix = log.rawData.length <= 12
|
||||
? log.rawData
|
||||
@@ -251,7 +336,8 @@ class AppProvider with ChangeNotifier {
|
||||
Future<void> _syncDrawingsOnStartup() async {
|
||||
// Wait for both MessagesProvider and DrawingProvider to finish initializing
|
||||
int attempts = 0;
|
||||
while ((!messagesProvider.isInitialized || !drawingProvider.isInitialized) &&
|
||||
while ((!messagesProvider.isInitialized ||
|
||||
!drawingProvider.isInitialized) &&
|
||||
attempts < 40) {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
attempts++;
|
||||
@@ -649,30 +735,6 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load auto-add discovered contacts setting from shared preferences.
|
||||
Future<void> _loadAutoAddDiscoveredContacts() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_autoAddDiscoveredContacts =
|
||||
prefs.getBool('auto_add_discovered_contacts') ?? false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error loading auto-add discovered contacts setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle auto-add discovered contacts on/off.
|
||||
Future<void> toggleAutoAddDiscoveredContacts(bool enabled) async {
|
||||
try {
|
||||
_autoAddDiscoveredContacts = enabled;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('auto_add_discovered_contacts', enabled);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving auto-add discovered contacts setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMessagingRouteSettings() async {
|
||||
try {
|
||||
_autoRouteRotationEnabled =
|
||||
@@ -1432,20 +1494,22 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (_autoAddDiscoveredContacts) {
|
||||
debugPrint(' Unknown contact - auto-add enabled, fetching details');
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
connectionProvider.getContact(publicKey);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
contactsProvider.addPendingAdvert(
|
||||
publicKey,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
debugPrint(
|
||||
' Unknown contact - added to pending adverts list and waiting for details',
|
||||
final isNewPendingAdvert = contactsProvider.addPendingAdvert(
|
||||
publicKey,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
debugPrint(
|
||||
' Unknown contact - added to pending adverts list and waiting for manual resolution',
|
||||
);
|
||||
if (isNewPendingAdvert) {
|
||||
final keyHex = publicKey
|
||||
.take(6)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
unawaited(
|
||||
_notificationService.showContactDiscoveredNotification(
|
||||
contactKey: keyHex,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3074,6 +3138,7 @@ class AppProvider with ChangeNotifier {
|
||||
_imageMissingRetryAttempts.clear();
|
||||
_voiceSessionSenderKey6.clear();
|
||||
_imageSessionSenderKey6.clear();
|
||||
_lowBatteryNotifiedNodeIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -3094,6 +3159,7 @@ class AppProvider with ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
_packetCaptureFlushTimer?.cancel();
|
||||
_lowBatteryCheckTimer?.cancel();
|
||||
unawaited(_flushPacketCaptureLogs());
|
||||
// Remove connection state listener
|
||||
connectionProvider.removeListener(_handleConnectionStateChange);
|
||||
|
||||
@@ -2255,7 +2255,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
throw Exception(_error);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2263,6 +2263,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
} catch (e) {
|
||||
_error = 'Failed to remove contact: $e';
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -66,6 +67,8 @@ class ContactsProvider with ChangeNotifier {
|
||||
bool _isInitialized = false;
|
||||
bool _isPersisting = false;
|
||||
bool _persistRequested = false;
|
||||
bool _isPersistingPendingAdverts = false;
|
||||
bool _persistPendingAdvertsRequested = false;
|
||||
|
||||
// Add default public channel on initialization
|
||||
ContactsProvider() {
|
||||
@@ -85,6 +88,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
final storedContacts = await _storageService.loadContacts();
|
||||
final storedGroups = await _storageService.loadContactGroups();
|
||||
final storedPendingAdverts = await _storageService.loadPendingAdverts();
|
||||
|
||||
// Add stored contacts (excluding any with all-zeros public key)
|
||||
const publicChannelKey =
|
||||
@@ -101,6 +105,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
_savedContactGroups
|
||||
..clear()
|
||||
..addAll(storedGroups);
|
||||
_restorePendingAdverts(storedPendingAdverts);
|
||||
debugPrint(
|
||||
'✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
||||
);
|
||||
@@ -133,6 +138,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
excludePublicKey: devicePublicKey,
|
||||
);
|
||||
final storedGroups = await _storageService.loadContactGroups();
|
||||
final storedPendingAdverts = await _storageService.loadPendingAdverts();
|
||||
|
||||
// Add stored contacts (excluding any with all-zeros public key)
|
||||
const publicChannelKey =
|
||||
@@ -149,6 +155,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
_savedContactGroups
|
||||
..clear()
|
||||
..addAll(storedGroups);
|
||||
_restorePendingAdverts(
|
||||
storedPendingAdverts,
|
||||
devicePublicKey: devicePublicKey,
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactsProvider] Loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
||||
);
|
||||
@@ -212,14 +222,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
try {
|
||||
while (_persistRequested) {
|
||||
_persistRequested = false;
|
||||
// Don't persist the public channel pseudo-contact (all zeros key)
|
||||
const publicChannelKey =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||
final contactsToSave = _contacts.entries
|
||||
.where((entry) => entry.key != publicChannelKey)
|
||||
.map((entry) => entry.value)
|
||||
.toList();
|
||||
await _storageService.saveContacts(contactsToSave);
|
||||
await _storageService.saveContacts(_contactsForStorage());
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error persisting contacts: $e');
|
||||
@@ -228,6 +231,24 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistPendingAdverts() async {
|
||||
_persistPendingAdvertsRequested = true;
|
||||
if (_isPersistingPendingAdverts) return;
|
||||
_isPersistingPendingAdverts = true;
|
||||
try {
|
||||
while (_persistPendingAdvertsRequested) {
|
||||
_persistPendingAdvertsRequested = false;
|
||||
await _storageService.savePendingAdverts(
|
||||
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactsProvider] Error persisting pending adverts: $e');
|
||||
} finally {
|
||||
_isPersistingPendingAdverts = false;
|
||||
}
|
||||
}
|
||||
|
||||
List<Contact> get contacts => _contacts.values.toList();
|
||||
List<SavedContactGroup> get savedContactGroups =>
|
||||
List<SavedContactGroup>.from(_savedContactGroups)
|
||||
@@ -450,6 +471,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
|
||||
);
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
debugPrint(' 🔔 notifyListeners() called');
|
||||
}
|
||||
@@ -481,6 +503,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1073,9 +1096,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
/// Add or refresh a pending advert entry from PUSH_CODE_ADVERT (0x80).
|
||||
/// Excludes self key and existing contacts.
|
||||
void addPendingAdvert(Uint8List publicKey, {Uint8List? devicePublicKey}) {
|
||||
bool addPendingAdvert(Uint8List publicKey, {Uint8List? devicePublicKey}) {
|
||||
if (devicePublicKey != null && publicKey.matches(devicePublicKey)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final keyHex = publicKey
|
||||
@@ -1083,20 +1106,26 @@ class ContactsProvider with ChangeNotifier {
|
||||
.join('');
|
||||
if (_contacts.containsKey(keyHex)) {
|
||||
_pendingAdverts.remove(keyHex);
|
||||
return;
|
||||
_persistPendingAdverts();
|
||||
return false;
|
||||
}
|
||||
|
||||
final existing = _pendingAdverts[keyHex];
|
||||
final now = DateTime.now();
|
||||
if (existing != null) {
|
||||
_pendingAdverts[keyHex] = existing.copyWith(receivedAt: now);
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
return false;
|
||||
} else {
|
||||
_pendingAdverts[keyHex] = PendingAdvert(
|
||||
publicKey: Uint8List.fromList(publicKey),
|
||||
receivedAt: now,
|
||||
);
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Find contact by name
|
||||
@@ -1155,6 +1184,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
_pendingAdverts.clear();
|
||||
_ensurePublicChannelExists();
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1177,6 +1207,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
_contacts.remove(publicKeyHex);
|
||||
_pendingAdverts.remove(publicKeyHex);
|
||||
_persistContacts();
|
||||
_persistPendingAdverts();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1185,6 +1216,74 @@ class ContactsProvider with ChangeNotifier {
|
||||
return await _storageService.getStorageStats();
|
||||
}
|
||||
|
||||
Future<void> persistNow() async {
|
||||
await _storageService.saveContacts(_contactsForStorage());
|
||||
}
|
||||
|
||||
List<Contact> _contactsForStorage() {
|
||||
// Don't persist the public channel pseudo-contact (all zeros key)
|
||||
const publicChannelKey =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||
return _contacts.entries
|
||||
.where((entry) => entry.key != publicChannelKey)
|
||||
.map((entry) => entry.value)
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _restorePendingAdverts(
|
||||
List<Map<String, dynamic>> storedPendingAdverts, {
|
||||
Uint8List? devicePublicKey,
|
||||
}) {
|
||||
_pendingAdverts.clear();
|
||||
for (final json in storedPendingAdverts) {
|
||||
final advert = _pendingAdvertFromJson(json);
|
||||
if (advert == null) continue;
|
||||
if (devicePublicKey != null &&
|
||||
advert.publicKey.matches(devicePublicKey)) {
|
||||
continue;
|
||||
}
|
||||
if (_contacts.containsKey(advert.publicKeyHex)) {
|
||||
continue;
|
||||
}
|
||||
_pendingAdverts[advert.publicKeyHex] = advert;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _pendingAdvertToJson(PendingAdvert advert) {
|
||||
return {
|
||||
'publicKey': base64Encode(advert.publicKey),
|
||||
'receivedAtMillis': advert.receivedAt.millisecondsSinceEpoch,
|
||||
'signedEncodedPathLen': advert.signedEncodedPathLen,
|
||||
'paddedPathBytes': advert.paddedPathBytes == null
|
||||
? null
|
||||
: base64Encode(advert.paddedPathBytes!),
|
||||
};
|
||||
}
|
||||
|
||||
PendingAdvert? _pendingAdvertFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return PendingAdvert(
|
||||
publicKey: Uint8List.fromList(
|
||||
base64Decode(json['publicKey'] as String),
|
||||
),
|
||||
receivedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['receivedAtMillis'] as int,
|
||||
),
|
||||
signedEncodedPathLen: json['signedEncodedPathLen'] as int?,
|
||||
paddedPathBytes: json['paddedPathBytes'] == null
|
||||
? null
|
||||
: Uint8List.fromList(
|
||||
base64Decode(json['paddedPathBytes'] as String),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'❌ [ContactsProvider] Error parsing pending advert from JSON: $e',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get contact count by type
|
||||
Map<String, int> get contactCounts {
|
||||
return {
|
||||
|
||||
@@ -18,6 +18,8 @@ import '../utils/image_message_parser.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'helpers/message_retry_manager.dart';
|
||||
|
||||
typedef DisplayMessageEntry = ({Message message, int occurrenceCount});
|
||||
|
||||
/// Messages Provider - manages message history and SAR markers
|
||||
class MessagesProvider with ChangeNotifier {
|
||||
static const Duration _channelEchoWarningDelay = Duration(seconds: 12);
|
||||
@@ -591,8 +593,12 @@ class MessagesProvider with ChangeNotifier {
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||
}
|
||||
_messageReceptionDetails[existingId] =
|
||||
MessageReceptionDetails.mergeDuplicate(
|
||||
existing: _messageReceptionDetails[existingId],
|
||||
incoming: receptionDetailsSnapshot,
|
||||
);
|
||||
if (receptionDetailsSnapshot != null) {
|
||||
_messageReceptionDetails[existingId] = receptionDetailsSnapshot;
|
||||
final existingMessage = _messages[duplicateIndex];
|
||||
final duplicatePathBytes = receptionDetailsSnapshot.pathBytes;
|
||||
if (existingMessage.pathBytes == null &&
|
||||
@@ -775,7 +781,13 @@ class MessagesProvider with ChangeNotifier {
|
||||
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
||||
|
||||
// Check for duplicates
|
||||
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
||||
final duplicateIndex = _findDuplicateMessageIndex(enhancedMessage);
|
||||
if (duplicateIndex != -1) {
|
||||
final existingId = _messages[duplicateIndex].id;
|
||||
_messageReceptionDetails[existingId] =
|
||||
MessageReceptionDetails.mergeDuplicate(
|
||||
existing: _messageReceptionDetails[existingId],
|
||||
);
|
||||
duplicateCount++;
|
||||
continue; // Skip duplicate
|
||||
}
|
||||
@@ -1026,7 +1038,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
/// overlapping serialization and redundant SharedPreferences writes.
|
||||
Future<void> _persistMessages() async {
|
||||
_persistRequested = true;
|
||||
if (_isPersisting) return; // A write is in flight; it will pick up our changes.
|
||||
if (_isPersisting) {
|
||||
return; // A write is in flight; it will pick up our changes.
|
||||
}
|
||||
_isPersisting = true;
|
||||
try {
|
||||
while (_persistRequested) {
|
||||
@@ -1121,6 +1135,35 @@ class MessagesProvider with ChangeNotifier {
|
||||
return sorted.take(count).toList();
|
||||
}
|
||||
|
||||
List<DisplayMessageEntry> buildDisplayMessages(Iterable<Message> messages) {
|
||||
final entries = <DisplayMessageEntry>[];
|
||||
|
||||
for (final message in messages) {
|
||||
final occurrenceCount = _messageOccurrenceCount(message);
|
||||
final existingIndex = entries.indexWhere(
|
||||
(entry) =>
|
||||
entry.message.text == message.text &&
|
||||
_matchesDuplicateScope(entry.message, message),
|
||||
);
|
||||
|
||||
if (existingIndex == -1) {
|
||||
entries.add((message: message, occurrenceCount: occurrenceCount));
|
||||
continue;
|
||||
}
|
||||
|
||||
final existingEntry = entries[existingIndex];
|
||||
entries[existingIndex] = (
|
||||
message: existingEntry.message,
|
||||
occurrenceCount: existingEntry.occurrenceCount + occurrenceCount,
|
||||
);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
int _messageOccurrenceCount(Message message) =>
|
||||
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
||||
|
||||
/// Get messages from last N hours
|
||||
List<Message> getMessagesSince(Duration duration) {
|
||||
final cutoff = DateTime.now().subtract(duration);
|
||||
|
||||
Reference in New Issue
Block a user