Add stats pills and RFC009 date

This commit is contained in:
Janez T
2026-03-05 08:12:48 +01:00
parent af2e969640
commit 6a52e7cfa1
4 changed files with 405 additions and 294 deletions

View File

@@ -489,7 +489,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 71;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 71;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -530,7 +530,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 71;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -547,7 +547,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 71;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -679,7 +679,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 71;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -702,7 +702,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 71;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -43,7 +43,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>70</string> <string>71</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>

View File

@@ -5,22 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000208"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.413276"> <testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.41002">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="113.573989"> <testcase classname="fastlane.lanes" name="2: build_app" time="107.00587">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="8345.856849"> <testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="232.858645">
</testcase> </testcase>

View File

@@ -58,10 +58,17 @@ class MessageBubble extends StatefulWidget {
class _MessageBubbleState extends State<MessageBubble> { class _MessageBubbleState extends State<MessageBubble> {
bool _isExpanded = false; bool _isExpanded = false;
bool _showReceivedStats = false;
@override @override
void didUpdateWidget(MessageBubble oldWidget) { void didUpdateWidget(MessageBubble oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.message.id != widget.message.id) {
_isExpanded = false;
_showReceivedStats = false;
return;
}
// Force rebuild when message properties change (especially recipient statuses) // Force rebuild when message properties change (especially recipient statuses)
if (oldWidget.message.id == widget.message.id) { if (oldWidget.message.id == widget.message.id) {
// Same message, but properties might have changed // Same message, but properties might have changed
@@ -77,6 +84,15 @@ class _MessageBubbleState extends State<MessageBubble> {
}); });
} }
void _handleBubbleTap({required bool isSarMarker, required bool isDrawing}) {
if (!widget.isCompact && !isSarMarker && !isDrawing) {
setState(() {
_showReceivedStats = !_showReceivedStats;
});
}
widget.onTap?.call();
}
Future<void> _retryFailedMessage( Future<void> _retryFailedMessage(
BuildContext context, BuildContext context,
Message failedMessage, Message failedMessage,
@@ -437,8 +453,8 @@ class _MessageBubbleState extends State<MessageBubble> {
final snrDb = final snrDb =
matchedRxLog?.logRxDataInfo?.snrDb ?? matchedRxLog?.logRxDataInfo?.snrDb ??
(widget.message.lastEchoSnrRaw != null (widget.message.lastEchoSnrRaw != null
? (widget.message.lastEchoSnrRaw!.toSigned(8) / 4.0) ? (widget.message.lastEchoSnrRaw!.toSigned(8) / 4.0)
: null); : null);
final rssiDbm = final rssiDbm =
matchedRxLog?.logRxDataInfo?.rssiDbm ?? widget.message.lastEchoRssiDbm; matchedRxLog?.logRxDataInfo?.rssiDbm ?? widget.message.lastEchoRssiDbm;
@@ -604,291 +620,305 @@ class _MessageBubbleState extends State<MessageBubble> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
_techBadge( _techBadge(
context,
icon: Icons.message,
label: widget.message.messageType.name.toUpperCase(),
),
_techBadge(
context,
icon: Icons.route,
label:
'${widget.message.pathLen} hop${widget.message.pathLen == 1 ? '' : 's'}',
),
_techBadge(
context,
icon: Icons.account_tree_outlined,
label:
'${widget.message.echoCount} node${widget.message.echoCount == 1 ? '' : 's'}',
),
if (widget.message.channelIdx != null)
_techBadge(
context,
icon: Icons.group_work,
label: 'CH ${widget.message.channelIdx}',
),
],
),
if (widget.message.lastEchoRssiDbm != null ||
snrDb != null ||
rssiDbm != null) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.network_check,
title: l10n.linkQuality,
child: Column(
children: [
if (rssiDbm != null)
_signalRow(
context, context,
label: 'RSSI', icon: Icons.message,
valueLabel: '$rssiDbm dBm', label: widget.message.messageType.name
normalized: .toUpperCase(),
((rssiDbm.toDouble() +
120.0) /
70.0)
.clamp(0.0, 1.0),
color: rssiDbm >= -80
? Colors.green
: rssiDbm >= -95
? Colors.amber
: Colors.redAccent,
), ),
if (snrDb != null) ...[ _techBadge(
const SizedBox(height: 8),
_signalRow(
context, context,
label: 'SNR', icon: Icons.route,
valueLabel: '${snrDb.toStringAsFixed(1)} dB', label:
normalized: ((snrDb + 20.0) / 40.0).clamp(0.0, 1.0), '${widget.message.pathLen} hop${widget.message.pathLen == 1 ? '' : 's'}',
color: snrDb >= 10 ),
? Colors.green _techBadge(
: snrDb >= 0 context,
? Colors.amber icon: Icons.account_tree_outlined,
: Colors.redAccent, label:
'${widget.message.echoCount} node${widget.message.echoCount == 1 ? '' : 's'}',
),
if (widget.message.channelIdx != null)
_techBadge(
context,
icon: Icons.group_work,
label: 'CH ${widget.message.channelIdx}',
),
],
),
if (widget.message.lastEchoRssiDbm != null ||
snrDb != null ||
rssiDbm != null) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.network_check,
title: l10n.linkQuality,
child: Column(
children: [
if (rssiDbm != null)
_signalRow(
context,
label: 'RSSI',
valueLabel: '$rssiDbm dBm',
normalized:
((rssiDbm.toDouble() + 120.0) / 70.0)
.clamp(0.0, 1.0),
color: rssiDbm >= -80
? Colors.green
: rssiDbm >= -95
? Colors.amber
: Colors.redAccent,
),
if (snrDb != null) ...[
const SizedBox(height: 8),
_signalRow(
context,
label: 'SNR',
valueLabel: '${snrDb.toStringAsFixed(1)} dB',
normalized: ((snrDb + 20.0) / 40.0).clamp(
0.0,
1.0,
),
color: snrDb >= 10
? Colors.green
: snrDb >= 0
? Colors.amber
: Colors.redAccent,
),
],
],
),
),
],
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.tune,
title: l10n.delivery,
child: Column(
children: [
_detailRow(
context,
label: l10n.status,
value: widget.message.deliveryStatus.name,
),
if (widget.message.expectedAckTag != null)
_detailRow(
context,
label: l10n.expectedAckTag,
value: widget.message.expectedAckTag!
.toString(),
),
if (widget.message.roundTripTimeMs != null)
_detailRow(
context,
label: l10n.roundTrip,
value: '${widget.message.roundTripTimeMs} ms',
),
if (widget.message.retryAttempt > 0)
_detailRow(
context,
label: l10n.retryAttempt,
value: widget.message.retryAttempt.toString(),
),
if (widget.message.usedFloodFallback)
_detailRow(
context,
label: l10n.floodFallback,
value: l10n.yes,
),
if (packetPathHex != null)
_detailRow(
context,
label: 'Path bytes',
value: packetPathHex,
onCopy: () => copyField(packetPathHex),
),
],
),
),
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.badge,
title: l10n.identity,
child: Column(
children: [
_detailRow(
context,
label: l10n.messageId,
value: widget.message.id,
onCopy: () => copyField(widget.message.id),
),
_detailRow(
context,
label: l10n.sender,
value:
senderName ??
widget.message.senderName ??
'Unknown',
),
if (senderPrefixHex != null)
_detailRow(
context,
label: l10n.senderKey,
value: senderPrefixHex,
onCopy: () => copyField(senderPrefixHex),
),
if (recipientName != null)
_detailRow(
context,
label: l10n.recipient,
value: recipientName,
),
if (recipientPrefixHex != null)
_detailRow(
context,
label: l10n.recipientKey,
value: recipientPrefixHex,
onCopy: () => copyField(recipientPrefixHex),
),
],
),
),
if (widget.message.isVoice) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.graphic_eq,
title: l10n.voice,
child: Column(
children: [
_detailRow(
context,
label: l10n.voiceId,
value: widget.message.voiceId ?? '-',
),
_detailRow(
context,
label: l10n.envelope,
value: envelope != null
? 'VE1 compact'
: legacyVoicePacket != null
? 'Legacy V packet'
: l10n.unknown,
),
if (voiceSession != null)
_detailRow(
context,
label: l10n.sessionProgress,
value:
'${voiceSession.receivedCount}/${voiceSession.total} segments',
),
if (voiceSession != null)
_detailRow(
context,
label: l10n.complete,
value: voiceSession.isComplete
? l10n.yes
: l10n.no,
),
if (voiceTxEstimate > Duration.zero)
_detailRow(
context,
label: 'Estimated tx',
value: voiceTxEstimate.inSeconds < 60
? '~${voiceTxEstimate.inSeconds}s'
: '~${voiceTxEstimate.inMinutes}m ${voiceTxEstimate.inSeconds % 60}s',
),
],
),
),
],
if (imageEnvelope != null) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.image_outlined,
title: 'Image',
child: Column(
children: [
_detailRow(
context,
label: l10n.envelope,
value: 'IE1',
),
_detailRow(
context,
label: 'Format',
value: imageEnvelope.format.label,
),
_detailRow(
context,
label: 'Dimensions',
value:
'${imageEnvelope.width}×${imageEnvelope.height}',
),
_detailRow(
context,
label: 'Segments',
value: imageSession != null
? '${imageSession.receivedCount}/${imageSession.total}'
: '${imageEnvelope.total}',
),
if (imageSession != null)
_detailRow(
context,
label: l10n.complete,
value: imageSession.isComplete
? l10n.yes
: l10n.no,
),
if (imageTxEstimate > Duration.zero)
_detailRow(
context,
label: 'Estimated tx',
value: imageTxEstimate.inSeconds < 60
? '~${imageTxEstimate.inSeconds}s'
: '~${imageTxEstimate.inMinutes}m ${imageTxEstimate.inSeconds % 60}s',
),
],
),
),
],
const SizedBox(height: 8),
ExpansionTile(
tilePadding: EdgeInsets.zero,
dense: true,
visualDensity: VisualDensity.compact,
title: Text(
l10n.rawDump,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
rawLines.join('\n'),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
), ),
], ],
],
),
),
],
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.tune,
title: l10n.delivery,
child: Column(
children: [
_detailRow(
context,
label: l10n.status,
value: widget.message.deliveryStatus.name,
), ),
if (widget.message.expectedAckTag != null)
_detailRow(
context,
label: l10n.expectedAckTag,
value: widget.message.expectedAckTag!.toString(),
),
if (widget.message.roundTripTimeMs != null)
_detailRow(
context,
label: l10n.roundTrip,
value: '${widget.message.roundTripTimeMs} ms',
),
if (widget.message.retryAttempt > 0)
_detailRow(
context,
label: l10n.retryAttempt,
value: widget.message.retryAttempt.toString(),
),
if (widget.message.usedFloodFallback)
_detailRow(
context,
label: l10n.floodFallback,
value: l10n.yes,
),
if (packetPathHex != null)
_detailRow(
context,
label: 'Path bytes',
value: packetPathHex,
onCopy: () => copyField(packetPathHex),
),
],
),
),
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.badge,
title: l10n.identity,
child: Column(
children: [
_detailRow(
context,
label: l10n.messageId,
value: widget.message.id,
onCopy: () => copyField(widget.message.id),
),
_detailRow(
context,
label: l10n.sender,
value: senderName ?? widget.message.senderName ?? 'Unknown',
),
if (senderPrefixHex != null)
_detailRow(
context,
label: l10n.senderKey,
value: senderPrefixHex,
onCopy: () => copyField(senderPrefixHex),
),
if (recipientName != null)
_detailRow(
context,
label: l10n.recipient,
value: recipientName,
),
if (recipientPrefixHex != null)
_detailRow(
context,
label: l10n.recipientKey,
value: recipientPrefixHex,
onCopy: () => copyField(recipientPrefixHex),
),
],
),
),
if (widget.message.isVoice) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.graphic_eq,
title: l10n.voice,
child: Column(
children: [
_detailRow(
context,
label: l10n.voiceId,
value: widget.message.voiceId ?? '-',
),
_detailRow(
context,
label: l10n.envelope,
value: envelope != null
? 'VE1 compact'
: legacyVoicePacket != null
? 'Legacy V packet'
: l10n.unknown,
),
if (voiceSession != null)
_detailRow(
context,
label: l10n.sessionProgress,
value:
'${voiceSession.receivedCount}/${voiceSession.total} segments',
),
if (voiceSession != null)
_detailRow(
context,
label: l10n.complete,
value: voiceSession.isComplete ? l10n.yes : l10n.no,
),
if (voiceTxEstimate > Duration.zero)
_detailRow(
context,
label: 'Estimated tx',
value: voiceTxEstimate.inSeconds < 60
? '~${voiceTxEstimate.inSeconds}s'
: '~${voiceTxEstimate.inMinutes}m ${voiceTxEstimate.inSeconds % 60}s',
),
],
),
),
],
if (imageEnvelope != null) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.image_outlined,
title: 'Image',
child: Column(
children: [
_detailRow(context, label: l10n.envelope, value: 'IE1'),
_detailRow(
context,
label: 'Format',
value: imageEnvelope.format.label,
),
_detailRow(
context,
label: 'Dimensions',
value:
'${imageEnvelope.width}×${imageEnvelope.height}',
),
_detailRow(
context,
label: 'Segments',
value: imageSession != null
? '${imageSession.receivedCount}/${imageSession.total}'
: '${imageEnvelope.total}',
),
if (imageSession != null)
_detailRow(
context,
label: l10n.complete,
value: imageSession.isComplete ? l10n.yes : l10n.no,
),
if (imageTxEstimate > Duration.zero)
_detailRow(
context,
label: 'Estimated tx',
value: imageTxEstimate.inSeconds < 60
? '~${imageTxEstimate.inSeconds}s'
: '~${imageTxEstimate.inMinutes}m ${imageTxEstimate.inSeconds % 60}s',
),
],
),
),
],
const SizedBox(height: 8),
ExpansionTile(
tilePadding: EdgeInsets.zero,
dense: true,
visualDensity: VisualDensity.compact,
title: Text(
l10n.rawDump,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
rawLines.join('\n'),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
],
),
], ],
), ),
), ),
@@ -1043,7 +1073,10 @@ class _MessageBubbleState extends State<MessageBubble> {
); );
} }
BlePacketLog? _findBestMatchingRxLog(List<BlePacketLog> logs, Message message) { BlePacketLog? _findBestMatchingRxLog(
List<BlePacketLog> logs,
Message message,
) {
if (message.pathLen < 0 || message.pathLen >= 255) return null; if (message.pathLen < 0 || message.pathLen >= 255) return null;
final expectedPayloadType = message.messageType == MessageType.channel final expectedPayloadType = message.messageType == MessageType.channel
? 0x05 ? 0x05
@@ -1465,6 +1498,55 @@ class _MessageBubbleState extends State<MessageBubble> {
); );
} }
Widget _buildReceivedSignalStatus(
BuildContext context,
Message message, {
required int? rssiDbm,
required double? snrDb,
}) {
final hopLabel = message.pathLen == 0
? 'Direct'
: '${message.pathLen} hop${message.pathLen == 1 ? '' : 's'}';
return Wrap(
spacing: 4,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
_techChip(
context,
icon: Icons.alt_route,
label: hopLabel,
color: Colors.indigo,
),
if (rssiDbm != null || snrDb != null) ...[
_techChip(
context,
icon: Icons.bolt,
label: _linkQualityLabel(rssiDbm, snrDb),
color: _linkQualityColor(_linkQualityLabel(rssiDbm, snrDb)),
),
if (rssiDbm != null)
_signalCapsule(
context,
icon: Icons.network_cell,
label: '$rssiDbm',
filled: _rssiScore(rssiDbm),
color: Colors.blueGrey,
),
if (snrDb != null)
_signalCapsule(
context,
icon: Icons.graphic_eq,
label: snrDb.toStringAsFixed(1),
filled: _snrScore(snrDb),
color: Colors.teal,
),
],
],
);
}
Widget _techChip( Widget _techChip(
BuildContext context, { BuildContext context, {
required IconData icon, required IconData icon,
@@ -1585,6 +1667,19 @@ class _MessageBubbleState extends State<MessageBubble> {
final selfPublicKey = connectionProvider.deviceInfo.publicKey; final selfPublicKey = connectionProvider.deviceInfo.publicKey;
final isOwnMessage = final isOwnMessage =
message.isSentMessage || message.isFromSelf(selfPublicKey); message.isSentMessage || message.isFromSelf(selfPublicKey);
final matchedRxLog = !isOwnMessage
? _findBestMatchingRxLog(
connectionProvider.bleService.packetLogs,
message,
)
: null;
final snrDb =
matchedRxLog?.logRxDataInfo?.snrDb ??
(message.lastEchoSnrRaw != null
? (message.lastEchoSnrRaw!.toSigned(8) / 4.0)
: null);
final rssiDbm =
matchedRxLog?.logRxDataInfo?.rssiDbm ?? message.lastEchoRssiDbm;
// Look up contact information for rich display name // Look up contact information for rich display name
final contactsProvider = context.read<ContactsProvider>(); final contactsProvider = context.read<ContactsProvider>();
@@ -1660,7 +1755,10 @@ class _MessageBubbleState extends State<MessageBubble> {
: recipientDisplayName; : recipientDisplayName;
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: () => _handleBubbleTap(
isSarMarker: isSarMarker,
isDrawing: message.isDrawing,
),
onLongPress: widget.isCompact ? null : () => _showMessageOptions(context), onLongPress: widget.isCompact ? null : () => _showMessageOptions(context),
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@@ -2065,6 +2163,19 @@ class _MessageBubbleState extends State<MessageBubble> {
else if (!message.isDrawing || widget.isCompact) else if (!message.isDrawing || widget.isCompact)
Text(message.text, style: Theme.of(context).textTheme.bodyMedium), Text(message.text, style: Theme.of(context).textTheme.bodyMedium),
if (!widget.isCompact &&
!isSarMarker &&
!message.isDrawing &&
_showReceivedStats) ...[
const SizedBox(height: 6),
_buildReceivedSignalStatus(
context,
message,
rssiDbm: rssiDbm,
snrDb: snrDb,
),
],
// Delivery status for sent messages (skip in compact mode) // Delivery status for sent messages (skip in compact mode)
if (message.isSentMessage && !widget.isCompact) ...[ if (message.isSentMessage && !widget.isCompact) ...[
const SizedBox(height: 6), const SizedBox(height: 6),