feat: Implement automatic reconnection handling and enhance message processing with sender identification

This commit is contained in:
Janez T
2025-10-15 14:53:55 +02:00
parent f50b429e34
commit 829b8605eb
9 changed files with 371 additions and 54 deletions

View File

@@ -165,6 +165,24 @@ class Message {
/// Check if this is a sent message (not received)
bool get isSentMessage => deliveryStatus != MessageDeliveryStatus.received;
/// Check if this message is from self (own message)
/// [selfPublicKey] - the device's own public key (first 6 bytes)
bool isFromSelf(Uint8List? selfPublicKey) {
if (selfPublicKey == null || selfPublicKey.length < 6) return false;
// Compare sender public key prefix with self public key prefix
if (senderPublicKeyPrefix != null && senderPublicKeyPrefix!.length >= 6) {
return senderPublicKeyPrefix![0] == selfPublicKey[0] &&
senderPublicKeyPrefix![1] == selfPublicKey[1] &&
senderPublicKeyPrefix![2] == selfPublicKey[2] &&
senderPublicKeyPrefix![3] == selfPublicKey[3] &&
senderPublicKeyPrefix![4] == selfPublicKey[4] &&
senderPublicKeyPrefix![5] == selfPublicKey[5];
}
return false;
}
Message copyWith({
String? id,
MessageType? messageType,

View File

@@ -53,7 +53,24 @@ class AppProvider with ChangeNotifier {
// When a message is received
connectionProvider.onMessageReceived = (message) {
messagesProvider.addMessage(message);
// Pass contact lookup function to link channel messages with contacts
messagesProvider.addMessage(
message,
contactLookup: (name) {
// Find contact by name and return their public key hex (first 12 chars for 6 bytes)
try {
final contact = contactsProvider.contacts.firstWhere(
(c) => c.advName == name,
);
return contact.publicKeyHex.isNotEmpty && contact.publicKeyHex.length >= 12
? contact.publicKeyHex.substring(0, 12)
: '';
} catch (e) {
// No matching contact found
return '';
}
},
);
// Optionally update sender name from contacts
if (message.senderPublicKeyPrefix != null) {

View File

@@ -42,6 +42,11 @@ class ConnectionProvider with ChangeNotifier {
int get rxPacketCount => _bleService.rxPacketCount;
int get txPacketCount => _bleService.txPacketCount;
// Reconnection state (exposed from BLE service)
bool get isReconnecting => _bleService.isReconnecting;
int get reconnectionAttempt => _bleService.reconnectionAttempt;
int get maxReconnectionAttempts => _bleService.maxReconnectionAttempts;
// Message sync state
bool _noMoreMessages = false;
@@ -75,15 +80,24 @@ class ConnectionProvider with ChangeNotifier {
_deviceInfo = _deviceInfo.copyWith(
connectionState: isConnected
? ConnectionState.connected
: ConnectionState.disconnected,
: (_bleService.isReconnecting
? ConnectionState.connecting
: ConnectionState.disconnected),
lastUpdate: DateTime.now(),
);
print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}');
print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}');
print(' isReconnecting: ${_bleService.isReconnecting}');
notifyListeners();
print(' Notified listeners');
};
_bleService.onReconnectionAttempt = (attemptNumber, maxAttempts) {
print('🔄 [Provider] Reconnection attempt $attemptNumber/$maxAttempts');
// Notify UI to update reconnection status display
notifyListeners();
};
_bleService.onError = (error) {
print('⚠️ [Provider] BLE error received: $error');
print(' Current connection state: ${_deviceInfo.connectionState}');
@@ -400,6 +414,13 @@ class ConnectionProvider with ChangeNotifier {
notifyListeners();
}
/// Cancel ongoing reconnection attempts
/// This is useful when the user wants to manually disconnect during reconnection
void cancelReconnection() {
print('🔴 [Provider] User requested cancellation of reconnection');
disconnect();
}
/// Get contacts from device
Future<void> getContacts() async {
if (!_bleService.isConnected) {
@@ -479,6 +500,8 @@ class ConnectionProvider with ChangeNotifier {
/// Send channel message
///
/// [messageId] - optional message ID to track delivery status
/// Note: Channel messages are ephemeral (not persisted), so they're marked
/// as "sent" immediately upon receiving OK response from the device.
Future<void> sendChannelMessage({
required int channelIdx,
required String text,
@@ -491,16 +514,19 @@ class ConnectionProvider with ChangeNotifier {
}
try {
// IMPORTANT: Track pending message BEFORE sending to avoid race condition
if (messageId != null) {
_messageDeliveryTracker.trackPendingMessage(messageId);
print(' Added message ID to pending queue BEFORE sending: $messageId');
}
await _bleService.sendChannelMessage(
channelIdx: channelIdx,
text: text,
);
// Channel messages are ephemeral (not persisted) - mark as "sent" immediately
// They don't have ACK/TAG mechanism like direct messages
if (messageId != null) {
print('✅ [Provider] Channel message sent successfully - marking as sent: $messageId');
// Use a dummy ACK tag (0) and timeout (0) for channel messages
// This will trigger the callback to mark the message as "sent"
onMessageSent?.call(messageId, 0, 0);
}
} catch (e) {
_error = 'Failed to send channel message: $e';
notifyListeners();

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import '../models/message.dart';
import '../models/sar_marker.dart';
@@ -79,15 +80,41 @@ class MessagesProvider with ChangeNotifier {
}
/// Add a message
void addMessage(Message message) {
/// If [contactLookup] function is provided, it will be used to match channel
/// message senders with known contacts by name
void addMessage(Message message, {String Function(String name)? contactLookup}) {
// Always enhance message with SAR parser to detect SAR markers
final enhancedMessage = SarMessageParser.enhanceMessage(message);
// For channel messages with sender name, try to link with contact
Message finalMessage = enhancedMessage;
if (enhancedMessage.isChannelMessage &&
enhancedMessage.senderName != null &&
contactLookup != null) {
// Look up contact public key by name
final publicKeyHex = contactLookup(enhancedMessage.senderName!);
if (publicKeyHex.isNotEmpty) {
// Convert hex string to bytes (first 6 bytes)
final publicKeyBytes = <int>[];
for (int i = 0; i < 12 && i < publicKeyHex.length; i += 2) {
final byteString = publicKeyHex.substring(i, i + 2);
publicKeyBytes.add(int.parse(byteString, radix: 16));
}
if (publicKeyBytes.length == 6) {
// Add public key prefix to message
finalMessage = enhancedMessage.copyWith(
senderPublicKeyPrefix: Uint8List.fromList(publicKeyBytes),
);
}
}
}
// Debug: Check if message is SAR
if (message.text.startsWith('S:')) {
print('🔍 [MessagesProvider] Processing SAR message: ${message.text}');
print(' isSarMarker: ${enhancedMessage.isSarMarker}');
print(' sarMarkerType: ${enhancedMessage.sarMarkerType}');
print(' isSarMarker: ${finalMessage.isSarMarker}');
print(' sarMarkerType: ${finalMessage.sarMarkerType}');
}
// Check for duplicates before adding
@@ -95,17 +122,17 @@ class MessagesProvider with ChangeNotifier {
// - Mesh network retransmissions
// - Multiple paths in the network
// - Syncing messages from device queue
if (_isDuplicate(enhancedMessage)) {
print('⚠️ [MessagesProvider] Duplicate message detected, skipping: ${enhancedMessage.id}');
print(' Text: ${enhancedMessage.text.substring(0, enhancedMessage.text.length > 50 ? 50 : enhancedMessage.text.length)}...');
if (_isDuplicate(finalMessage)) {
print('⚠️ [MessagesProvider] Duplicate message detected, skipping: ${finalMessage.id}');
print(' Text: ${finalMessage.text.substring(0, finalMessage.text.length > 50 ? 50 : finalMessage.text.length)}...');
return; // Skip duplicate
}
_messages.add(enhancedMessage);
_messages.add(finalMessage);
// If it's a SAR marker message, extract and store the marker
if (enhancedMessage.isSarMarker) {
final marker = enhancedMessage.toSarMarker();
if (finalMessage.isSarMarker) {
final marker = finalMessage.toSarMarker();
if (marker != null) {
_sarMarkers[marker.id] = marker;
}
@@ -348,33 +375,40 @@ class MessagesProvider with ChangeNotifier {
if (index != -1) {
final message = _messages[index];
print(' Current status: ${message.deliveryStatus}');
print(' Message type: ${message.messageType}');
print(' Message text preview: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...');
final updatedMessage = message.copyWith(
deliveryStatus: MessageDeliveryStatus.sent,
expectedAckTag: expectedAckTag,
suggestedTimeoutMs: suggestedTimeoutMs,
expectedAckTag: expectedAckTag > 0 ? expectedAckTag : null,
suggestedTimeoutMs: suggestedTimeoutMs > 0 ? suggestedTimeoutMs : null,
);
_messages[index] = updatedMessage;
// Track by ACK tag for matching with delivery confirmation
_pendingSentMessages[expectedAckTag] = updatedMessage;
print(' ✅ Added to pending messages map with ACK: $expectedAckTag');
print(' Total pending messages: ${_pendingSentMessages.length}');
print(' Pending ACKs after adding: ${_pendingSentMessages.keys.toList()}');
// Only track and set timeout for direct messages (channel messages have expectedAckTag=0)
if (expectedAckTag > 0 && suggestedTimeoutMs > 0) {
// Track by ACK tag for matching with delivery confirmation
_pendingSentMessages[expectedAckTag] = updatedMessage;
print(' ✅ Added to pending messages map with ACK: $expectedAckTag');
print(' Total pending messages: ${_pendingSentMessages.length}');
print(' Pending ACKs after adding: ${_pendingSentMessages.keys.toList()}');
// Start timeout timer
_timeoutTimers[expectedAckTag] = Timer(
Duration(milliseconds: suggestedTimeoutMs),
() {
print('⏱️ [MessagesProvider] Timeout for message $messageId (ACK $expectedAckTag)');
if (_pendingSentMessages.containsKey(expectedAckTag)) {
markMessageFailed(messageId);
}
},
);
// Start timeout timer
_timeoutTimers[expectedAckTag] = Timer(
Duration(milliseconds: suggestedTimeoutMs),
() {
print('⏱️ [MessagesProvider] Timeout for message $messageId (ACK $expectedAckTag)');
if (_pendingSentMessages.containsKey(expectedAckTag)) {
markMessageFailed(messageId);
}
},
);
print('⏱️ [MessagesProvider] Started ${suggestedTimeoutMs}ms timeout timer for message $messageId (ACK $expectedAckTag)');
} else {
print(' Channel message (no ACK tracking) - marked as sent immediately');
}
print('⏱️ [MessagesProvider] Started ${suggestedTimeoutMs}ms timeout timer for message $messageId (ACK $expectedAckTag)');
print(' Calling notifyListeners() to update UI with "sent" status');
_persistMessages();

View File

@@ -335,28 +335,64 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
Text(
isConnected
? deviceInfo.displayName ?? 'Connected'
: 'Disconnected',
: (provider.isReconnecting
? 'Reconnecting... (${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts})'
: 'Disconnected'),
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
color: provider.isReconnecting
? Colors.orange[600]
: Colors.grey[600],
),
),
],
),
),
if (!isConnected)
ElevatedButton.icon(
onPressed: () => _showConnectionDialog(context),
icon: const Icon(Icons.bluetooth, size: 18),
label: const Text('Connect'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: provider.isReconnecting
? null // Disable button during reconnection
: () => _showConnectionDialog(context),
icon: provider.isReconnecting
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.black54),
),
)
: const Icon(Icons.bluetooth, size: 18),
label: Text(provider.isReconnecting
? 'Reconnecting (${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts})'
: 'Connect'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
),
// Cancel button during reconnection
if (provider.isReconnecting) ...[
const SizedBox(width: 8),
IconButton(
onPressed: () => provider.cancelReconnection(),
icon: const Icon(Icons.close, size: 20),
tooltip: 'Cancel reconnection',
style: IconButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.all(8),
),
),
],
],
)
else
Row(

View File

@@ -503,6 +503,11 @@ class _MessageBubble extends StatelessWidget {
final isSarMarker = message.isSarMarker;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// Get device's own public key from ConnectionProvider
final connectionProvider = context.read<ConnectionProvider>();
final selfPublicKey = connectionProvider.deviceInfo.publicKey;
final isOwnMessage = message.isFromSelf(selfPublicKey);
// Debug: Log message details
if (message.text.startsWith('S:')) {
debugPrint('🎨 [MessageBubble] Rendering SAR message:');
@@ -519,14 +524,19 @@ class _MessageBubble extends StatelessWidget {
decoration: BoxDecoration(
color: isSarMarker
? _getSarMarkerColor(context, isDarkMode)
: Theme.of(context).colorScheme.surfaceVariant,
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode),
borderRadius: BorderRadius.circular(16),
border: isSarMarker
? Border.all(
color: _getSarMarkerBorderColor(context, isDarkMode),
width: 3,
)
: null,
: isOwnMessage
? Border.all(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
width: 2,
)
: null,
boxShadow: isSarMarker
? [
BoxShadow(
@@ -574,15 +584,18 @@ class _MessageBubble extends StatelessWidget {
),
)
else ...[
if (message.isChannelMessage)
if (isOwnMessage)
Icon(Icons.account_circle, size: 16, color: Theme.of(context).colorScheme.primary)
else if (message.isChannelMessage)
const Icon(Icons.tag, size: 16)
else
const Icon(Icons.person, size: 16),
const SizedBox(width: 4),
Text(
message.displaySender,
isOwnMessage ? 'You' : message.displaySender,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isOwnMessage ? Theme.of(context).colorScheme.primary : null,
),
),
],
@@ -735,6 +748,18 @@ class _MessageBubble extends StatelessWidget {
}
}
Color _getMessageBubbleColor(BuildContext context, bool isOwnMessage, bool isDarkMode) {
if (isOwnMessage) {
// Own messages: slightly highlighted with primary color tint
return isDarkMode
? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3)
: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.15);
} else {
// Others' messages: default surface color
return Theme.of(context).colorScheme.surfaceVariant;
}
}
Color _getSarMarkerColor(BuildContext context, bool isDarkMode) {
if (message.sarMarkerType == null) {
return Theme.of(context).colorScheme.primaryContainer;

View File

@@ -5,20 +5,34 @@ import '../meshcore_constants.dart';
/// Callback types for connection events
typedef OnConnectionStateCallback = void Function(bool isConnected);
typedef OnErrorCallback = void Function(String error);
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
/// Manages BLE connection lifecycle
/// Manages BLE connection lifecycle with automatic reconnection
class BleConnectionManager {
BluetoothDevice? _device;
BluetoothCharacteristic? _rxCharacteristic;
BluetoothCharacteristic? _txCharacteristic;
bool _isConnected = false;
// Reconnection state
bool _reconnectionEnabled = true;
bool _isReconnecting = false;
int _reconnectionAttempt = 0;
Timer? _reconnectionTimer;
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
static const int _maxReconnectionAttempts = 5;
static const List<int> _reconnectionDelaysMs = [1000, 2000, 3000, 5000, 10000]; // Exponential backoff
// Callbacks
OnConnectionStateCallback? onConnectionStateChanged;
OnErrorCallback? onError;
OnReconnectionAttemptCallback? onReconnectionAttempt;
// Getters
bool get isConnected => _isConnected;
bool get isReconnecting => _isReconnecting;
int get reconnectionAttempt => _reconnectionAttempt;
int get maxReconnectionAttempts => _maxReconnectionAttempts;
BluetoothDevice? get device => _device;
BluetoothCharacteristic? get rxCharacteristic => _rxCharacteristic;
BluetoothCharacteristic? get txCharacteristic => _txCharacteristic;
@@ -138,9 +152,13 @@ class BleConnectionManager {
print('✅ [BLE] Notifications enabled');
_isConnected = true;
_reconnectionAttempt = 0; // Reset reconnection counter on successful connection
print('🔵 [BLE] Notifying connection state change: connected');
onConnectionStateChanged?.call(true);
// Monitor connection state for automatic reconnection
_setupConnectionMonitoring();
print('✅✅✅ [BLE] Connection completed successfully!');
return true;
} catch (e) {
@@ -156,6 +174,11 @@ class BleConnectionManager {
/// Disconnect from device
Future<void> disconnect() async {
try {
print('🔴 [BLE] Disconnect requested by user');
// Disable reconnection before disconnecting
_reconnectionEnabled = false;
_cancelReconnection();
await _device?.disconnect();
_isConnected = false;
_device = null;
@@ -167,8 +190,126 @@ class BleConnectionManager {
}
}
/// Setup connection monitoring for automatic reconnection
void _setupConnectionMonitoring() {
print('🔵 [BLE] Setting up connection monitoring for device: ${_device?.platformName}');
// Cancel any existing subscription
_connectionStateSubscription?.cancel();
// Monitor connection state changes
_connectionStateSubscription = _device?.connectionState.listen((state) {
print('🔔 [BLE] Connection state changed: $state');
if (state == BluetoothConnectionState.disconnected) {
print('⚠️ [BLE] Device disconnected unexpectedly!');
_isConnected = false;
onConnectionStateChanged?.call(false);
// Attempt automatic reconnection if enabled
if (_reconnectionEnabled && !_isReconnecting) {
print('🔄 [BLE] Starting automatic reconnection...');
_attemptReconnection();
}
} else if (state == BluetoothConnectionState.connected) {
print('✅ [BLE] Device connected');
_isConnected = true;
_reconnectionAttempt = 0;
_isReconnecting = false;
onConnectionStateChanged?.call(true);
}
});
}
/// Attempt to reconnect to the device
Future<void> _attemptReconnection() async {
if (_device == null || _isReconnecting || !_reconnectionEnabled) {
return;
}
_isReconnecting = true;
_reconnectionAttempt++;
print('🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts');
onReconnectionAttempt?.call(_reconnectionAttempt, _maxReconnectionAttempts);
if (_reconnectionAttempt > _maxReconnectionAttempts) {
print('❌ [BLE] Max reconnection attempts reached. Giving up.');
_isReconnecting = false;
onError?.call('Connection lost. Max reconnection attempts ($_maxReconnectionAttempts) reached.');
return;
}
// Calculate delay with exponential backoff
final delayIndex = (_reconnectionAttempt - 1).clamp(0, _reconnectionDelaysMs.length - 1);
final delayMs = _reconnectionDelaysMs[delayIndex];
print('🔄 [BLE] Waiting ${delayMs}ms before reconnection attempt $_reconnectionAttempt...');
// Wait before attempting reconnection
_reconnectionTimer = Timer(Duration(milliseconds: delayMs), () async {
if (!_reconnectionEnabled) {
print('🔄 [BLE] Reconnection cancelled by user');
_isReconnecting = false;
return;
}
try {
print('🔄 [BLE] Attempting to reconnect...');
// Try to reconnect
final success = await connect(_device!);
if (success) {
print('✅ [BLE] Reconnection successful!');
_isReconnecting = false;
_reconnectionAttempt = 0;
} else {
print('❌ [BLE] Reconnection attempt $_reconnectionAttempt failed');
_isReconnecting = false;
// Try again if we haven't reached max attempts
if (_reconnectionAttempt < _maxReconnectionAttempts) {
_attemptReconnection();
} else {
onError?.call('Connection lost. Unable to reconnect after $_maxReconnectionAttempts attempts.');
}
}
} catch (e) {
print('❌ [BLE] Reconnection attempt $_reconnectionAttempt error: $e');
_isReconnecting = false;
// Try again if we haven't reached max attempts
if (_reconnectionAttempt < _maxReconnectionAttempts) {
_attemptReconnection();
} else {
onError?.call('Connection lost. Unable to reconnect: $e');
}
}
});
}
/// Cancel ongoing reconnection attempts
void _cancelReconnection() {
print('🔴 [BLE] Cancelling reconnection attempts');
_reconnectionTimer?.cancel();
_reconnectionTimer = null;
_isReconnecting = false;
_reconnectionAttempt = 0;
_connectionStateSubscription?.cancel();
_connectionStateSubscription = null;
}
/// Enable automatic reconnection (useful after user manually disconnects)
void enableReconnection() {
print('🔵 [BLE] Re-enabling automatic reconnection');
_reconnectionEnabled = true;
}
/// Dispose resources
void dispose() {
print('🔴 [BLE] Disposing BLE connection manager');
_cancelReconnection();
_device = null;
_rxCharacteristic = null;
_txCharacteristic = null;

View File

@@ -30,6 +30,7 @@ typedef OnBinaryResponseCallback = void Function(Uint8List publicKeyPrefix, int
typedef OnBatteryAndStorageCallback = void Function(int millivolts, int? usedKb, int? totalKb);
typedef OnErrorCallback = void Function(String error);
typedef OnConnectionStateCallback = void Function(bool isConnected);
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
/// MeshCore BLE Service - coordinates BLE communication components
class MeshCoreBleService {
@@ -40,6 +41,7 @@ class MeshCoreBleService {
// Event callbacks
OnConnectionStateCallback? onConnectionStateChanged;
OnReconnectionAttemptCallback? onReconnectionAttempt;
OnContactCallback? onContactReceived;
OnContactsCompleteCallback? onContactsComplete;
OnMessageCallback? onMessageReceived;
@@ -77,6 +79,10 @@ class MeshCoreBleService {
_connectionManager.onError = (error) {
onError?.call(error);
};
_connectionManager.onReconnectionAttempt = (attemptNumber, maxAttempts) {
print('🔄 [Service] Reconnection attempt $attemptNumber/$maxAttempts');
onReconnectionAttempt?.call(attemptNumber, maxAttempts);
};
// Command sender callbacks
_commandSender.onError = (error) {
@@ -148,6 +154,9 @@ class MeshCoreBleService {
// Getters
bool get isConnected => _connectionManager.isConnected;
bool get isReconnecting => _connectionManager.isReconnecting;
int get reconnectionAttempt => _connectionManager.reconnectionAttempt;
int get maxReconnectionAttempts => _connectionManager.maxReconnectionAttempts;
int get rxPacketCount => _responseHandler.rxPacketCount;
int get txPacketCount => _commandSender.txPacketCount;
List<BlePacketLog> get packetLogs {

View File

@@ -111,6 +111,16 @@ class FrameParser {
text = reader.readString();
}
// Parse sender name from channel message format: "<sender_name>: <actual_message>"
String? senderName;
String actualMessage = text;
if (text.contains(': ')) {
final colonIndex = text.indexOf(': ');
senderName = text.substring(0, colonIndex);
actualMessage = text.substring(colonIndex + 2); // Skip ": "
}
return Message(
id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx',
messageType: MessageType.channel,
@@ -118,7 +128,8 @@ class FrameParser {
pathLen: pathLen,
textType: txtType,
senderTimestamp: senderTimestamp,
text: text,
text: actualMessage, // Store the actual message without sender prefix
senderName: senderName, // Store extracted sender name
receivedAt: DateTime.now(),
);
}