Fix message bubble layout

This commit is contained in:
Janez T
2026-03-07 08:28:22 +01:00
parent f1d56e2000
commit b0eded3780
4 changed files with 249 additions and 68 deletions

View File

@@ -1616,19 +1616,13 @@ class _MessageBubbleState extends State<MessageBubble> {
}
Widget _buildChannelEchoStatus(BuildContext context, Message message) {
final statusColor = _getDeliveryStatusColor(message.deliveryStatus);
final hasEcho = message.echoCount > 0;
if (!hasEcho) {
return Text(
message.getLocalizedDeliveryStatus(context),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: statusColor,
fontStyle: FontStyle.italic,
),
);
return const SizedBox.shrink();
}
final statusColor = _getDeliveryStatusColor(message.deliveryStatus);
final rssi = message.lastEchoRssiDbm;
final snr = message.lastEchoSnrRaw != null
? message.lastEchoSnrRaw!.toSigned(8) / 4.0
@@ -1792,6 +1786,40 @@ class _MessageBubbleState extends State<MessageBubble> {
);
}
Widget _buildDirectHeaderCounterpart(
BuildContext context, {
required bool isOwnMessage,
required String label,
}) {
final textColor = Theme.of(
context,
).textTheme.labelSmall?.color?.withValues(alpha: 0.75);
return Row(
children: [
Icon(
isOwnMessage ? Icons.arrow_forward : Icons.arrow_back,
size: 12,
color: Theme.of(
context,
).textTheme.labelSmall?.color?.withValues(alpha: 0.7),
),
const SizedBox(width: 4),
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: textColor,
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
String _hopDebugLabel(Message message) {
if (message.pathLen >= 255 && message.isContactMessage) {
return 'Direct (raw: ${message.pathLen})';
@@ -2097,6 +2125,9 @@ class _MessageBubbleState extends State<MessageBubble> {
isOwnMessage && message.isChannelMessage && recipientDisplayName != null
? '${l10n.channel}: $recipientDisplayName'
: recipientDisplayName;
final directCounterpartLabel = !message.isChannelMessage
? (isOwnMessage ? recipientSubtitle : l10n.you)
: null;
final receivedChannelSubtitle =
!isOwnMessage && message.isChannelMessage && channelDisplayName != null
? '${l10n.channel}: $channelDisplayName'
@@ -2293,7 +2324,7 @@ class _MessageBubbleState extends State<MessageBubble> {
Expanded(
child: Row(
children: [
Flexible(
Expanded(
child: Text(
displayName,
style: Theme.of(context).textTheme.labelMedium
@@ -2309,10 +2340,12 @@ class _MessageBubbleState extends State<MessageBubble> {
),
if (!widget.isCompact &&
(recipientSubtitle != null ||
directCounterpartLabel != null ||
receivedChannelSubtitle != null)) ...[
const SizedBox(width: 8),
if (message.isChannelMessage)
Flexible(
Align(
alignment: Alignment.centerRight,
child: _buildChannelHeaderPill(
context,
label: isOwnMessage
@@ -2322,41 +2355,10 @@ class _MessageBubbleState extends State<MessageBubble> {
)
else
Expanded(
child: Row(
children: [
Icon(
isOwnMessage
? Icons.arrow_forward
: Icons.arrow_back,
size: 12,
color: Theme.of(context)
.textTheme
.labelSmall
?.color
?.withValues(alpha: 0.7),
),
const SizedBox(width: 4),
Expanded(
child: Text(
isOwnMessage
? recipientSubtitle!
: receivedChannelSubtitle!,
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: Theme.of(context)
.textTheme
.labelSmall
?.color
?.withValues(alpha: 0.75),
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
child: _buildDirectHeaderCounterpart(
context,
isOwnMessage: isOwnMessage,
label: directCounterpartLabel!,
),
),
],
@@ -2838,7 +2840,8 @@ class _MessageBubbleState extends State<MessageBubble> {
),
]
// Show single message delivery status
else
else if (!message.isChannelMessage ||
message.deliveryStatus == MessageDeliveryStatus.failed)
Row(
mainAxisSize: MainAxisSize.max,
children: [

View File

@@ -51,7 +51,7 @@ class MessagesComposer extends StatelessWidget {
SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.fromLTRB(10, 10, 10, bottomPadding),
padding: EdgeInsets.fromLTRB(10, 4, 10, bottomPadding),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,

View File

@@ -281,12 +281,29 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
.sublist(0, 6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
final request = VoiceFetchRequest(
final missing = voiceProvider.missingPacketIndices(sessionId);
final totalPackets = sessionPacketCount(
voiceProvider: voiceProvider,
sessionId: sessionId,
requesterKey6: requesterKey6,
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
version: 2,
envelope: envelope,
);
final isPartialResume =
missing.isNotEmpty && totalPackets > 0 && missing.length < totalPackets;
final request = isPartialResume
? VoiceFetchRequest(
sessionId: sessionId,
want: 'missing',
missingIndices: missing,
requesterKey6: requesterKey6,
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
version: 2,
)
: VoiceFetchRequest(
sessionId: sessionId,
requesterKey6: requesterKey6,
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
version: 2,
);
setState(() {
_isRequesting = true;
@@ -309,11 +326,18 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
final effectivePathLen = sender.outPathLen >= 0
? sender.outPathLen
: pathLen;
final estimatedDurationMs =
envelope != null &&
totalPackets > 0 &&
missing.isNotEmpty &&
missing.length < totalPackets
? ((envelope.durationMs * missing.length) / totalPackets).round()
: envelope?.durationMs;
final txEstimate = envelope != null
? estimateVoiceTransmitDuration(
packetCount: envelope.total,
packetCount: isPartialResume ? missing.length : envelope.total,
mode: envelope.mode,
durationMs: envelope.durationMs,
durationMs: estimatedDurationMs ?? envelope.durationMs,
pathLen: effectivePathLen,
radioBw: radioBw,
radioSf: radioSf,
@@ -331,6 +355,14 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
);
}
int sessionPacketCount({
required VoiceProvider voiceProvider,
required String sessionId,
required VoiceEnvelope? envelope,
}) {
return voiceProvider.session(sessionId)?.total ?? envelope?.total ?? 0;
}
void _setUnavailable() {
if (!mounted) return;
_showToast(AppLocalizations.of(context)!.voiceUnavailable);