mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
Add transmission timing details
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'connection_provider.dart';
|
||||
@@ -16,9 +17,12 @@ import '../services/packet_capture_storage_service.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/ble_packet_log.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../utils/drawing_message_parser.dart';
|
||||
import '../utils/raw_route_probe.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
import '../utils/message_airtime_estimator.dart';
|
||||
|
||||
/// Main App Provider - coordinates all other providers
|
||||
class AppProvider with ChangeNotifier {
|
||||
@@ -70,6 +74,8 @@ class AppProvider with ChangeNotifier {
|
||||
FragmentAckWaitRegistry();
|
||||
final FragmentAckWaitRegistry _imageFragmentAckWaiters =
|
||||
FragmentAckWaitRegistry();
|
||||
final FragmentAckWaitRegistry _rawProbeWaiters = FragmentAckWaitRegistry();
|
||||
final Map<String, Future<bool>> _pendingRawRouteProbes = {};
|
||||
Timer? _packetCaptureFlushTimer;
|
||||
String? _lastPersistedPacketSignature;
|
||||
bool _isPersistingPacketCapture = false;
|
||||
@@ -633,6 +639,9 @@ class AppProvider with ChangeNotifier {
|
||||
capturedAt: enrichedMessage.receivedAt,
|
||||
)
|
||||
: null;
|
||||
final receptionDetailsSnapshot = _buildReceptionDetailsSnapshot(
|
||||
enrichedMessage,
|
||||
);
|
||||
|
||||
// Check if message is a drawing broadcast
|
||||
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
|
||||
@@ -664,6 +673,7 @@ class AppProvider with ChangeNotifier {
|
||||
updatedMessage,
|
||||
contactLookup: (name) => '',
|
||||
contactLocationSnapshot: contactLocationSnapshot,
|
||||
receptionDetailsSnapshot: receptionDetailsSnapshot,
|
||||
);
|
||||
|
||||
// Broadcast drawing message to SSE clients if server is running
|
||||
@@ -700,6 +710,7 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
},
|
||||
contactLocationSnapshot: contactLocationSnapshot,
|
||||
receptionDetailsSnapshot: receptionDetailsSnapshot,
|
||||
);
|
||||
connectionProvider.broadcastMessageToSseClients(enrichedMessage);
|
||||
return;
|
||||
@@ -728,6 +739,7 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
},
|
||||
contactLocationSnapshot: contactLocationSnapshot,
|
||||
receptionDetailsSnapshot: receptionDetailsSnapshot,
|
||||
);
|
||||
connectionProvider.broadcastMessageToSseClients(enrichedMessage);
|
||||
return;
|
||||
@@ -765,12 +777,16 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
},
|
||||
contactLocationSnapshot: contactLocationSnapshot,
|
||||
receptionDetailsSnapshot: receptionDetailsSnapshot,
|
||||
);
|
||||
|
||||
// Broadcast message to SSE clients if server is running
|
||||
connectionProvider.broadcastMessageToSseClients(enrichedMessage);
|
||||
};
|
||||
|
||||
// Keep a compact receive-time snapshot because packet logs roll over.
|
||||
// This lets the UI still show timing/link details after app restarts.
|
||||
|
||||
// When telemetry is received via PUSH_CODE_TELEMETRY_RESPONSE (0x8B)
|
||||
// Used by older firmware versions for telemetry responses
|
||||
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
|
||||
@@ -796,6 +812,18 @@ class AppProvider with ChangeNotifier {
|
||||
// Magic 0x72 'r' = voice fetch request; 0x69 'i' = image fetch request.
|
||||
// Magic 0x56 'V' = voice packet; magic 0x49 'I' = image packet.
|
||||
connectionProvider.onRawDataReceived = (payload, snrRaw, rssiDbm) {
|
||||
final rawProbeRequest = RawRouteProbeRequest.tryParseBinary(payload);
|
||||
if (rawProbeRequest != null) {
|
||||
_handleRawRouteProbeRequest(rawProbeRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
final rawProbeAck = RawRouteProbeAck.tryParseBinary(payload);
|
||||
if (rawProbeAck != null) {
|
||||
_completeRawRouteProbeAck(rawProbeAck.nonce);
|
||||
return;
|
||||
}
|
||||
|
||||
final voiceFetchRequest = VoiceFetchRequest.tryParseBinary(payload);
|
||||
if (voiceFetchRequest != null) {
|
||||
final requester = _resolveVoiceFetchRequester(voiceFetchRequest);
|
||||
@@ -1559,6 +1587,78 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
String _fragmentAckKey(String sessionId, int index) => '$sessionId:$index';
|
||||
String _rawProbeKey(int nonce) =>
|
||||
nonce.toRadixString(16).padLeft(8, '0').toLowerCase();
|
||||
|
||||
Future<bool> verifyRawTransportRoute(
|
||||
Contact target, {
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
return false;
|
||||
}
|
||||
if (target.outPathLen < 0 || target.outPathLen > _maxDirectPayloadHops) {
|
||||
return false;
|
||||
}
|
||||
if (target.outPath.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final probeKey = _routeProbeTargetKey(target);
|
||||
final pendingProbe = _pendingRawRouteProbes[probeKey];
|
||||
if (pendingProbe != null) {
|
||||
return pendingProbe;
|
||||
}
|
||||
|
||||
final deviceKey = connectionProvider.deviceInfo.publicKey;
|
||||
if (deviceKey == null || deviceKey.length < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final requesterKey6 = deviceKey
|
||||
.sublist(0, 6)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final future = () async {
|
||||
final nonce = math.Random.secure().nextInt(0x100000000);
|
||||
final ackFuture = _rawProbeWaiters.waitFor(
|
||||
_rawProbeKey(nonce),
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
try {
|
||||
await connectionProvider.sendRawVoicePacket(
|
||||
contactPath: target.outPath,
|
||||
contactPathLen: target.outPathLen,
|
||||
payload: RawRouteProbeRequest(
|
||||
nonce: nonce,
|
||||
requesterKey6: requesterKey6,
|
||||
).encodeBinary(),
|
||||
);
|
||||
return await ackFuture;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'⚠️ [AppProvider] Raw route probe failed for ${target.advName}: $e',
|
||||
);
|
||||
_rawProbeWaiters.complete(_rawProbeKey(nonce));
|
||||
return false;
|
||||
}
|
||||
}();
|
||||
|
||||
_pendingRawRouteProbes[probeKey] = future;
|
||||
try {
|
||||
return await future;
|
||||
} finally {
|
||||
_pendingRawRouteProbes.remove(probeKey);
|
||||
}
|
||||
}
|
||||
|
||||
String _routeProbeTargetKey(Contact target) {
|
||||
if (target.publicKeyHex.isNotEmpty) {
|
||||
return 'pk:${target.publicKeyHex}';
|
||||
}
|
||||
return 'name:${target.advName}:${target.outPathLen}:${target.outPath.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
|
||||
}
|
||||
|
||||
Future<bool> _waitForVoiceFragmentAck({
|
||||
required String sessionId,
|
||||
@@ -1608,6 +1708,37 @@ class AppProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleRawRouteProbeRequest(RawRouteProbeRequest request) {
|
||||
final requester = _resolveContactByPrefixHex(request.requesterKey6);
|
||||
if (requester == null) {
|
||||
debugPrint(
|
||||
'⚠️ [AppProvider] Raw route probe requester not found: ${request.requesterKey6}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (requester.outPathLen < 0 ||
|
||||
requester.outPathLen > _maxDirectPayloadHops) {
|
||||
debugPrint(
|
||||
'⚠️ [AppProvider] Raw route probe requester out of range: ${requester.outPathLen}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (requester.outPath.isEmpty) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
connectionProvider.sendRawVoicePacket(
|
||||
contactPath: requester.outPath,
|
||||
contactPathLen: requester.outPathLen,
|
||||
payload: RawRouteProbeAck(nonce: request.nonce).encodeBinary(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _completeRawRouteProbeAck(int nonce) {
|
||||
_rawProbeWaiters.complete(_rawProbeKey(nonce));
|
||||
}
|
||||
|
||||
void _sendVoiceFragmentAck(VoicePacket packet) {
|
||||
final senderKey6 = _voiceSessionSenderKey6[packet.sessionId];
|
||||
if (senderKey6 == null) return;
|
||||
@@ -1648,6 +1779,95 @@ class AppProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
MessageReceptionDetails? _buildReceptionDetailsSnapshot(Message message) {
|
||||
final matchedRxLog = _findBestMatchingRxLog(message);
|
||||
final estimatedTx = estimateMessageTransmitDuration(
|
||||
message,
|
||||
radioBw: connectionProvider.deviceInfo.radioBw,
|
||||
radioSf: connectionProvider.deviceInfo.radioSf,
|
||||
radioCr: connectionProvider.deviceInfo.radioCr,
|
||||
);
|
||||
final senderToReceiptMs = _senderToReceiptMs(message);
|
||||
final estimatedTransmitMs = estimatedTx > Duration.zero
|
||||
? estimatedTx.inMilliseconds
|
||||
: null;
|
||||
final postTransmitDelayMs =
|
||||
senderToReceiptMs != null && estimatedTransmitMs != null
|
||||
? (senderToReceiptMs - estimatedTransmitMs).clamp(0, 86400000).toInt()
|
||||
: null;
|
||||
|
||||
if (matchedRxLog == null &&
|
||||
senderToReceiptMs == null &&
|
||||
estimatedTransmitMs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MessageReceptionDetails(
|
||||
capturedAt: DateTime.now(),
|
||||
packetLoggedAt: matchedRxLog?.timestamp,
|
||||
rssiDbm: matchedRxLog?.logRxDataInfo?.rssiDbm,
|
||||
snrDb: matchedRxLog?.logRxDataInfo?.snrDb,
|
||||
pathBytes: _extractPathBytesFromLog(matchedRxLog),
|
||||
senderToReceiptMs: senderToReceiptMs,
|
||||
estimatedTransmitMs: estimatedTransmitMs,
|
||||
postTransmitDelayMs: postTransmitDelayMs,
|
||||
);
|
||||
}
|
||||
|
||||
int? _senderToReceiptMs(Message message) {
|
||||
if (message.senderTimestamp <= 0) return null;
|
||||
final senderAt = DateTime.fromMillisecondsSinceEpoch(
|
||||
message.senderTimestamp * 1000,
|
||||
isUtc: true,
|
||||
);
|
||||
final deltaMs = message.receivedAt
|
||||
.toUtc()
|
||||
.difference(senderAt)
|
||||
.inMilliseconds;
|
||||
if (deltaMs < 0 || deltaMs > 86400000) return null;
|
||||
return deltaMs;
|
||||
}
|
||||
|
||||
BlePacketLog? _findBestMatchingRxLog(Message message) {
|
||||
if (message.pathLen < 0 || message.pathLen >= 255) return null;
|
||||
final expectedPayloadType = message.messageType == MessageType.channel
|
||||
? 0x05
|
||||
: 0x02;
|
||||
BlePacketLog? bestLog;
|
||||
var bestDeltaMs = 999999999;
|
||||
|
||||
for (final log in connectionProvider.bleService.packetLogs) {
|
||||
if (log.responseCode != 0x88) continue;
|
||||
if (log.rawData.length < 6) continue;
|
||||
|
||||
final raw = log.rawData;
|
||||
final payloadType = (raw[3] >> 2) & 0x0F;
|
||||
final pathLen = raw[4];
|
||||
if (payloadType != expectedPayloadType) continue;
|
||||
if (pathLen != message.pathLen) continue;
|
||||
if (raw.length < 5 + pathLen) continue;
|
||||
|
||||
final deltaMs =
|
||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||
if (deltaMs < bestDeltaMs) {
|
||||
bestDeltaMs = deltaMs;
|
||||
bestLog = log;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDeltaMs > 30000) return null;
|
||||
return bestLog;
|
||||
}
|
||||
|
||||
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
||||
if (log == null) return null;
|
||||
final raw = log.rawData;
|
||||
if (raw.length < 6) return null;
|
||||
final pathLen = raw[4];
|
||||
if (pathLen <= 0 || raw.length < 5 + pathLen) return null;
|
||||
return raw.sublist(5, 5 + pathLen);
|
||||
}
|
||||
|
||||
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching
|
||||
|
||||
|
||||
@@ -146,11 +146,15 @@ class ConnectionProvider with ChangeNotifier {
|
||||
final MessageDeliveryTracker _messageDeliveryTracker =
|
||||
MessageDeliveryTracker();
|
||||
final PingTracker _pingTracker = PingTracker();
|
||||
final Map<String, Future<PingResult>> _pendingSmartPings = {};
|
||||
|
||||
// Expose room login states
|
||||
Map<String, RoomLoginState> get roomLoginStates =>
|
||||
_roomLoginManager.roomLoginStates;
|
||||
|
||||
bool isPingInProgress(Uint8List publicKey) =>
|
||||
_pendingSmartPings.containsKey(_publicKeyToHex(publicKey));
|
||||
|
||||
// Callbacks for other providers
|
||||
Function(Contact)? onContactReceived;
|
||||
Function(List<Contact>)? onContactsComplete;
|
||||
@@ -1316,6 +1320,34 @@ class ConnectionProvider with ChangeNotifier {
|
||||
required Uint8List contactPublicKey,
|
||||
required bool hasPath,
|
||||
Function()? onRetryWithFlooding,
|
||||
}) async {
|
||||
final pingKey = _publicKeyToHex(contactPublicKey);
|
||||
final pendingPing = _pendingSmartPings[pingKey];
|
||||
if (pendingPing != null) {
|
||||
debugPrint('ℹ️ [Provider] Joining in-flight ping for $pingKey');
|
||||
return pendingPing;
|
||||
}
|
||||
|
||||
final future = _runSmartPing(
|
||||
contactPublicKey: contactPublicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: onRetryWithFlooding,
|
||||
);
|
||||
_pendingSmartPings[pingKey] = future;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
return await future;
|
||||
} finally {
|
||||
_pendingSmartPings.remove(pingKey);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<PingResult> _runSmartPing({
|
||||
required Uint8List contactPublicKey,
|
||||
required bool hasPath,
|
||||
Function()? onRetryWithFlooding,
|
||||
}) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
@@ -1334,7 +1366,10 @@ class ConnectionProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
// Send the ping
|
||||
await _activeService.requestTelemetry(contactPublicKey, zeroHop: true);
|
||||
await _activeService.requestTelemetry(
|
||||
contactPublicKey,
|
||||
zeroHop: firstAttemptDirect,
|
||||
);
|
||||
|
||||
// Wait for response or timeout
|
||||
final bool gotResponse = await pingFuture;
|
||||
@@ -1361,8 +1396,8 @@ class ConnectionProvider with ChangeNotifier {
|
||||
wasDirectAttempt: false,
|
||||
);
|
||||
|
||||
// Retry with flooding (zeroHop=true acts as broadcast to neighbors)
|
||||
await _activeService.requestTelemetry(contactPublicKey, zeroHop: true);
|
||||
// Retry with flooding.
|
||||
await _activeService.requestTelemetry(contactPublicKey, zeroHop: false);
|
||||
|
||||
// Wait for response or timeout
|
||||
final bool gotRetryResponse = await retryFuture;
|
||||
@@ -1384,6 +1419,10 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String _publicKeyToHex(Uint8List publicKey) {
|
||||
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||
}
|
||||
|
||||
/// Send binary request to contact (modern replacement for requestTelemetry)
|
||||
///
|
||||
/// Supports multiple request types:
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message_contact_location.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../services/message_storage_service.dart';
|
||||
@@ -22,6 +23,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
bool _isInitialized = false;
|
||||
AppLocalizations? _localizations;
|
||||
final Map<String, MessageContactLocation> _messageContactLocations = {};
|
||||
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
||||
|
||||
// Track pending sent messages by expected ACK/TAG
|
||||
final Map<int, Message> _pendingSentMessages = {};
|
||||
@@ -74,10 +76,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
})?
|
||||
sendMessageCallback;
|
||||
|
||||
Future<void> Function({
|
||||
required Contact contact,
|
||||
required int failureStreak,
|
||||
})?
|
||||
Future<void> Function({required Contact contact, required int failureStreak})?
|
||||
onDirectPathFailedCallback;
|
||||
|
||||
List<Message> get messages => List.unmodifiable(_messages);
|
||||
@@ -115,6 +114,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
MessageContactLocation? getMessageContactLocation(String messageId) =>
|
||||
_messageContactLocations[messageId];
|
||||
|
||||
MessageReceptionDetails? getMessageReceptionDetails(String messageId) =>
|
||||
_messageReceptionDetails[messageId];
|
||||
|
||||
/// Set localizations for notifications
|
||||
void setLocalizations(AppLocalizations localizations) {
|
||||
_localizations = localizations;
|
||||
@@ -145,9 +147,14 @@ class MessagesProvider with ChangeNotifier {
|
||||
final storedMessages = await _storageService.loadMessages();
|
||||
final storedContactLocations = await _storageService
|
||||
.loadMessageContactLocations();
|
||||
final storedReceptionDetails = await _storageService
|
||||
.loadMessageReceptionDetails();
|
||||
_messageContactLocations
|
||||
..clear()
|
||||
..addAll(storedContactLocations);
|
||||
_messageReceptionDetails
|
||||
..clear()
|
||||
..addAll(storedReceptionDetails);
|
||||
|
||||
// Add stored messages with enhancement to ensure SAR detection
|
||||
for (final message in storedMessages) {
|
||||
@@ -311,6 +318,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
Message message, {
|
||||
String Function(String name)? contactLookup,
|
||||
MessageContactLocation? contactLocationSnapshot,
|
||||
MessageReceptionDetails? receptionDetailsSnapshot,
|
||||
}) {
|
||||
// Always enhance message with SAR parser to detect SAR markers
|
||||
var enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
@@ -397,6 +405,22 @@ class MessagesProvider with ChangeNotifier {
|
||||
debugPrint(
|
||||
' Text: ${finalMessage.text.substring(0, finalMessage.text.length > 50 ? 50 : finalMessage.text.length)}...',
|
||||
);
|
||||
final existingIndex = _messages.indexWhere(
|
||||
(existing) =>
|
||||
existing.messageType == finalMessage.messageType &&
|
||||
existing.senderTimestamp == finalMessage.senderTimestamp &&
|
||||
existing.text == finalMessage.text,
|
||||
);
|
||||
if (existingIndex != -1) {
|
||||
final existingId = _messages[existingIndex].id;
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||
}
|
||||
if (receptionDetailsSnapshot != null) {
|
||||
_messageReceptionDetails[existingId] = receptionDetailsSnapshot;
|
||||
}
|
||||
_persistMessages();
|
||||
}
|
||||
return; // Skip duplicate
|
||||
}
|
||||
|
||||
@@ -404,6 +428,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[finalMessage.id] = contactLocationSnapshot;
|
||||
}
|
||||
if (receptionDetailsSnapshot != null) {
|
||||
_messageReceptionDetails[finalMessage.id] = receptionDetailsSnapshot;
|
||||
}
|
||||
|
||||
// If it's a SAR marker message, extract and store the marker
|
||||
if (finalMessage.isSarMarker) {
|
||||
@@ -593,6 +620,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
await _storageService.saveMessages(
|
||||
_messages,
|
||||
messageContactLocations: _messageContactLocations,
|
||||
messageReceptionDetails: _messageReceptionDetails,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
|
||||
@@ -709,6 +737,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageContactMap.remove(messageId);
|
||||
_groupedMessageMapping.remove(messageId);
|
||||
_messageContactLocations.remove(messageId);
|
||||
_messageReceptionDetails.remove(messageId);
|
||||
|
||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||
|
||||
@@ -738,6 +767,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messages.clear();
|
||||
_sarMarkers.clear();
|
||||
_messageContactLocations.clear();
|
||||
_messageReceptionDetails.clear();
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -753,6 +783,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messages.clear();
|
||||
_sarMarkers.clear();
|
||||
_messageContactLocations.clear();
|
||||
_messageReceptionDetails.clear();
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -1062,10 +1093,11 @@ class MessagesProvider with ChangeNotifier {
|
||||
' Message text preview: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...',
|
||||
);
|
||||
|
||||
// Once the device accepts a direct message and returns an ACK tag, the
|
||||
// send itself succeeded locally even if end-to-end delivery confirmation
|
||||
// may still arrive later. Keep ACK tracking, but stop showing "waiting".
|
||||
final updatedMessage = message.copyWith(
|
||||
deliveryStatus: expectedAckTag > 0
|
||||
? MessageDeliveryStatus.sending
|
||||
: MessageDeliveryStatus.sent,
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
expectedAckTag: expectedAckTag > 0 ? expectedAckTag : null,
|
||||
suggestedTimeoutMs: expectedAckTag > 0 ? effectiveTimeout : null,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user