mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Fix message bubble layout
This commit is contained in:
@@ -64,6 +64,8 @@ class AppProvider with ChangeNotifier {
|
|||||||
final Map<String, String> _imageSessionSenderKey6 = {};
|
final Map<String, String> _imageSessionSenderKey6 = {};
|
||||||
final Map<String, Timer> _voiceMissingRetryTimers = {};
|
final Map<String, Timer> _voiceMissingRetryTimers = {};
|
||||||
final Map<String, int> _voiceMissingRetryAttempts = {};
|
final Map<String, int> _voiceMissingRetryAttempts = {};
|
||||||
|
final Map<String, Timer> _imageMissingRetryTimers = {};
|
||||||
|
final Map<String, int> _imageMissingRetryAttempts = {};
|
||||||
final FragmentAckWaitRegistry _voiceFragmentAckWaiters =
|
final FragmentAckWaitRegistry _voiceFragmentAckWaiters =
|
||||||
FragmentAckWaitRegistry();
|
FragmentAckWaitRegistry();
|
||||||
final FragmentAckWaitRegistry _imageFragmentAckWaiters =
|
final FragmentAckWaitRegistry _imageFragmentAckWaiters =
|
||||||
@@ -796,9 +798,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
connectionProvider.onRawDataReceived = (payload, snrRaw, rssiDbm) {
|
connectionProvider.onRawDataReceived = (payload, snrRaw, rssiDbm) {
|
||||||
final voiceFetchRequest = VoiceFetchRequest.tryParseBinary(payload);
|
final voiceFetchRequest = VoiceFetchRequest.tryParseBinary(payload);
|
||||||
if (voiceFetchRequest != null) {
|
if (voiceFetchRequest != null) {
|
||||||
final requester = contactsProvider.findContactByPrefixHex(
|
final requester = _resolveVoiceFetchRequester(voiceFetchRequest);
|
||||||
voiceFetchRequest.requesterKey6,
|
|
||||||
);
|
|
||||||
if (requester == null) {
|
if (requester == null) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [AppProvider] Voice fetch requester contact not found (binary)',
|
'⚠️ [AppProvider] Voice fetch requester contact not found (binary)',
|
||||||
@@ -901,6 +901,10 @@ class AppProvider with ChangeNotifier {
|
|||||||
height: session?.height ?? 0,
|
height: session?.height ?? 0,
|
||||||
);
|
);
|
||||||
_sendImageFragmentAck(frag);
|
_sendImageFragmentAck(frag);
|
||||||
|
_scheduleImageMissingRetry(
|
||||||
|
frag.sessionId,
|
||||||
|
justComplete: imageProvider.isComplete(frag.sessionId),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1268,15 +1272,51 @@ class AppProvider with ChangeNotifier {
|
|||||||
return contactsProvider.findContactByPrefixHex(prefixHex.toLowerCase());
|
return contactsProvider.findContactByPrefixHex(prefixHex.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Contact? _resolveVoiceFetchRequester(VoiceFetchRequest request) {
|
||||||
|
final liveContact = _resolveContactByPrefixHex(request.requesterKey6);
|
||||||
|
if (liveContact != null) {
|
||||||
|
return liveContact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _resolveRequesterFromSentMessages(
|
||||||
|
sessionId: request.sessionId,
|
||||||
|
requesterKey6: request.requesterKey6,
|
||||||
|
tryParseEnvelope: VoiceEnvelope.tryParseText,
|
||||||
|
mediaLabel: 'voice',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Contact? _resolveImageFetchRequester(ImageFetchRequest request) {
|
Contact? _resolveImageFetchRequester(ImageFetchRequest request) {
|
||||||
final liveContact = _resolveContactByPrefixHex(request.requesterKey6);
|
final liveContact = _resolveContactByPrefixHex(request.requesterKey6);
|
||||||
if (liveContact != null) {
|
if (liveContact != null) {
|
||||||
return liveContact;
|
return liveContact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _resolveRequesterFromSentMessages(
|
||||||
|
sessionId: request.sessionId,
|
||||||
|
requesterKey6: request.requesterKey6,
|
||||||
|
tryParseEnvelope: ImageEnvelope.tryParse,
|
||||||
|
mediaLabel: 'image',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact? _resolveRequesterFromSentMessages<T>({
|
||||||
|
required String sessionId,
|
||||||
|
required String requesterKey6,
|
||||||
|
required T? Function(String text) tryParseEnvelope,
|
||||||
|
required String mediaLabel,
|
||||||
|
}) {
|
||||||
for (final message in messagesProvider.messages.reversed) {
|
for (final message in messagesProvider.messages.reversed) {
|
||||||
final envelope = ImageEnvelope.tryParse(message.text);
|
final envelope = tryParseEnvelope(message.text);
|
||||||
if (envelope == null || envelope.sessionId != request.sessionId) {
|
if (envelope == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final envelopeSessionId = switch (envelope) {
|
||||||
|
VoiceEnvelope voiceEnvelope => voiceEnvelope.sessionId,
|
||||||
|
ImageEnvelope imageEnvelope => imageEnvelope.sessionId,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (envelopeSessionId != sessionId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final recipientKey = message.recipientPublicKey;
|
final recipientKey = message.recipientPublicKey;
|
||||||
@@ -1291,12 +1331,13 @@ class AppProvider with ChangeNotifier {
|
|||||||
.sublist(0, 6)
|
.sublist(0, 6)
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
.join();
|
.join();
|
||||||
if (recipientKey6 != request.requesterKey6) {
|
if (recipientKey6 != requesterKey6) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📷 [AppProvider] Resolved image requester from sent message metadata '
|
'${mediaLabel == 'voice' ? '🎙️' : '📷'} [AppProvider] Resolved '
|
||||||
'for session ${request.sessionId}: ${recipient.advName}',
|
'$mediaLabel requester from sent message metadata for session '
|
||||||
|
'$sessionId: ${recipient.advName}',
|
||||||
);
|
);
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
@@ -1308,9 +1349,12 @@ class AppProvider with ChangeNotifier {
|
|||||||
String sessionId, {
|
String sessionId, {
|
||||||
required bool justComplete,
|
required bool justComplete,
|
||||||
}) {
|
}) {
|
||||||
|
if (voiceProvider.isReceiveCanceled(sessionId)) {
|
||||||
|
_clearVoiceMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (justComplete || voiceProvider.isComplete(sessionId)) {
|
if (justComplete || voiceProvider.isComplete(sessionId)) {
|
||||||
_voiceMissingRetryTimers.remove(sessionId)?.cancel();
|
_clearVoiceMissingRetry(sessionId);
|
||||||
_voiceMissingRetryAttempts.remove(sessionId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,10 +1365,43 @@ class AppProvider with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestMissingVoicePackets(String sessionId) async {
|
void _scheduleImageMissingRetry(
|
||||||
if (voiceProvider.isComplete(sessionId)) {
|
String sessionId, {
|
||||||
|
required bool justComplete,
|
||||||
|
}) {
|
||||||
|
if (imageProvider.isReceiveCanceled(sessionId)) {
|
||||||
|
_clearImageMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (justComplete || imageProvider.isComplete(sessionId)) {
|
||||||
|
_clearImageMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_imageMissingRetryAttempts[sessionId] = 0;
|
||||||
|
_imageMissingRetryTimers[sessionId]?.cancel();
|
||||||
|
_imageMissingRetryTimers[sessionId] = Timer(_packetRetryDelay, () {
|
||||||
|
unawaited(_requestMissingImageFragments(sessionId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearVoiceMissingRetry(String sessionId) {
|
||||||
_voiceMissingRetryTimers.remove(sessionId)?.cancel();
|
_voiceMissingRetryTimers.remove(sessionId)?.cancel();
|
||||||
_voiceMissingRetryAttempts.remove(sessionId);
|
_voiceMissingRetryAttempts.remove(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearImageMissingRetry(String sessionId) {
|
||||||
|
_imageMissingRetryTimers.remove(sessionId)?.cancel();
|
||||||
|
_imageMissingRetryAttempts.remove(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestMissingVoicePackets(String sessionId) async {
|
||||||
|
if (voiceProvider.isReceiveCanceled(sessionId)) {
|
||||||
|
_clearVoiceMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (voiceProvider.isComplete(sessionId)) {
|
||||||
|
_clearVoiceMissingRetry(sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1333,7 +1410,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [AppProvider] Voice re-request limit reached for $sessionId',
|
'⚠️ [AppProvider] Voice re-request limit reached for $sessionId',
|
||||||
);
|
);
|
||||||
_voiceMissingRetryTimers.remove(sessionId)?.cancel();
|
_clearVoiceMissingRetry(sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1345,8 +1422,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
|
|
||||||
final missing = voiceProvider.missingPacketIndices(sessionId);
|
final missing = voiceProvider.missingPacketIndices(sessionId);
|
||||||
if (missing.isEmpty) {
|
if (missing.isEmpty) {
|
||||||
_voiceMissingRetryTimers.remove(sessionId)?.cancel();
|
_clearVoiceMissingRetry(sessionId);
|
||||||
_voiceMissingRetryAttempts.remove(sessionId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1381,6 +1457,67 @@ class AppProvider with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _requestMissingImageFragments(String sessionId) async {
|
||||||
|
if (imageProvider.isReceiveCanceled(sessionId)) {
|
||||||
|
_clearImageMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (imageProvider.isComplete(sessionId)) {
|
||||||
|
_clearImageMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final attempt = _imageMissingRetryAttempts[sessionId] ?? 0;
|
||||||
|
if (attempt >= _maxPacketRetryAttempts) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [AppProvider] Image re-request limit reached for $sessionId',
|
||||||
|
);
|
||||||
|
_clearImageMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final senderKey6 = _imageSessionSenderKey6[sessionId];
|
||||||
|
if (senderKey6 == null) return;
|
||||||
|
final sender = _resolveContactByPrefixHex(senderKey6);
|
||||||
|
final deviceKey = connectionProvider.deviceInfo.publicKey;
|
||||||
|
if (sender == null || deviceKey == null || deviceKey.length < 6) return;
|
||||||
|
|
||||||
|
final missing = imageProvider.missingFragmentIndices(sessionId);
|
||||||
|
if (missing.isEmpty) {
|
||||||
|
_clearImageMissingRetry(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final requesterKey6 = deviceKey
|
||||||
|
.sublist(0, 6)
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
final request = ImageFetchRequest(
|
||||||
|
sessionId: sessionId,
|
||||||
|
want: 'missing',
|
||||||
|
missingIndices: missing,
|
||||||
|
requesterKey6: requesterKey6,
|
||||||
|
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connectionProvider.sendRawVoicePacket(
|
||||||
|
contactPath: sender.outPath,
|
||||||
|
contactPathLen: sender.outPathLen,
|
||||||
|
payload: request.encodeBinary(),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_imageMissingRetryAttempts[sessionId] = attempt + 1;
|
||||||
|
_imageMissingRetryTimers[sessionId]?.cancel();
|
||||||
|
_imageMissingRetryTimers[sessionId] = Timer(_packetRetryDelay, () {
|
||||||
|
unawaited(_requestMissingImageFragments(sessionId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert or update a voice placeholder message for binary raw-data packets.
|
/// Insert or update a voice placeholder message for binary raw-data packets.
|
||||||
///
|
///
|
||||||
/// Binary voice packets arrive without a chat message, so we synthesise one
|
/// Binary voice packets arrive without a chat message, so we synthesise one
|
||||||
@@ -1604,9 +1741,15 @@ class AppProvider with ChangeNotifier {
|
|||||||
for (final timer in _voiceMissingRetryTimers.values) {
|
for (final timer in _voiceMissingRetryTimers.values) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
|
for (final timer in _imageMissingRetryTimers.values) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
_voiceMissingRetryTimers.clear();
|
_voiceMissingRetryTimers.clear();
|
||||||
_voiceMissingRetryAttempts.clear();
|
_voiceMissingRetryAttempts.clear();
|
||||||
|
_imageMissingRetryTimers.clear();
|
||||||
|
_imageMissingRetryAttempts.clear();
|
||||||
_voiceSessionSenderKey6.clear();
|
_voiceSessionSenderKey6.clear();
|
||||||
|
_imageSessionSenderKey6.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1640,6 +1783,9 @@ class AppProvider with ChangeNotifier {
|
|||||||
for (final timer in _voiceMissingRetryTimers.values) {
|
for (final timer in _voiceMissingRetryTimers.values) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
|
for (final timer in _imageMissingRetryTimers.values) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1616,19 +1616,13 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChannelEchoStatus(BuildContext context, Message message) {
|
Widget _buildChannelEchoStatus(BuildContext context, Message message) {
|
||||||
final statusColor = _getDeliveryStatusColor(message.deliveryStatus);
|
|
||||||
final hasEcho = message.echoCount > 0;
|
final hasEcho = message.echoCount > 0;
|
||||||
|
|
||||||
if (!hasEcho) {
|
if (!hasEcho) {
|
||||||
return Text(
|
return const SizedBox.shrink();
|
||||||
message.getLocalizedDeliveryStatus(context),
|
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
||||||
color: statusColor,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final statusColor = _getDeliveryStatusColor(message.deliveryStatus);
|
||||||
final rssi = message.lastEchoRssiDbm;
|
final rssi = message.lastEchoRssiDbm;
|
||||||
final snr = message.lastEchoSnrRaw != null
|
final snr = message.lastEchoSnrRaw != null
|
||||||
? message.lastEchoSnrRaw!.toSigned(8) / 4.0
|
? 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) {
|
String _hopDebugLabel(Message message) {
|
||||||
if (message.pathLen >= 255 && message.isContactMessage) {
|
if (message.pathLen >= 255 && message.isContactMessage) {
|
||||||
return 'Direct (raw: ${message.pathLen})';
|
return 'Direct (raw: ${message.pathLen})';
|
||||||
@@ -2097,6 +2125,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
isOwnMessage && message.isChannelMessage && recipientDisplayName != null
|
isOwnMessage && message.isChannelMessage && recipientDisplayName != null
|
||||||
? '${l10n.channel}: $recipientDisplayName'
|
? '${l10n.channel}: $recipientDisplayName'
|
||||||
: recipientDisplayName;
|
: recipientDisplayName;
|
||||||
|
final directCounterpartLabel = !message.isChannelMessage
|
||||||
|
? (isOwnMessage ? recipientSubtitle : l10n.you)
|
||||||
|
: null;
|
||||||
final receivedChannelSubtitle =
|
final receivedChannelSubtitle =
|
||||||
!isOwnMessage && message.isChannelMessage && channelDisplayName != null
|
!isOwnMessage && message.isChannelMessage && channelDisplayName != null
|
||||||
? '${l10n.channel}: $channelDisplayName'
|
? '${l10n.channel}: $channelDisplayName'
|
||||||
@@ -2293,7 +2324,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
displayName,
|
displayName,
|
||||||
style: Theme.of(context).textTheme.labelMedium
|
style: Theme.of(context).textTheme.labelMedium
|
||||||
@@ -2309,10 +2340,12 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
),
|
),
|
||||||
if (!widget.isCompact &&
|
if (!widget.isCompact &&
|
||||||
(recipientSubtitle != null ||
|
(recipientSubtitle != null ||
|
||||||
|
directCounterpartLabel != null ||
|
||||||
receivedChannelSubtitle != null)) ...[
|
receivedChannelSubtitle != null)) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (message.isChannelMessage)
|
if (message.isChannelMessage)
|
||||||
Flexible(
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
child: _buildChannelHeaderPill(
|
child: _buildChannelHeaderPill(
|
||||||
context,
|
context,
|
||||||
label: isOwnMessage
|
label: isOwnMessage
|
||||||
@@ -2322,41 +2355,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: _buildDirectHeaderCounterpart(
|
||||||
children: [
|
context,
|
||||||
Icon(
|
isOwnMessage: isOwnMessage,
|
||||||
isOwnMessage
|
label: directCounterpartLabel!,
|
||||||
? 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2838,7 +2840,8 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
// Show single message delivery status
|
// Show single message delivery status
|
||||||
else
|
else if (!message.isChannelMessage ||
|
||||||
|
message.deliveryStatus == MessageDeliveryStatus.failed)
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class MessagesComposer extends StatelessWidget {
|
|||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(10, 10, 10, bottomPadding),
|
padding: EdgeInsets.fromLTRB(10, 4, 10, bottomPadding),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
|||||||
@@ -281,7 +281,24 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
.sublist(0, 6)
|
.sublist(0, 6)
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
.join('');
|
.join('');
|
||||||
final request = VoiceFetchRequest(
|
final missing = voiceProvider.missingPacketIndices(sessionId);
|
||||||
|
final totalPackets = sessionPacketCount(
|
||||||
|
voiceProvider: voiceProvider,
|
||||||
|
sessionId: sessionId,
|
||||||
|
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,
|
sessionId: sessionId,
|
||||||
requesterKey6: requesterKey6,
|
requesterKey6: requesterKey6,
|
||||||
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
@@ -309,11 +326,18 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
final effectivePathLen = sender.outPathLen >= 0
|
final effectivePathLen = sender.outPathLen >= 0
|
||||||
? sender.outPathLen
|
? sender.outPathLen
|
||||||
: pathLen;
|
: pathLen;
|
||||||
|
final estimatedDurationMs =
|
||||||
|
envelope != null &&
|
||||||
|
totalPackets > 0 &&
|
||||||
|
missing.isNotEmpty &&
|
||||||
|
missing.length < totalPackets
|
||||||
|
? ((envelope.durationMs * missing.length) / totalPackets).round()
|
||||||
|
: envelope?.durationMs;
|
||||||
final txEstimate = envelope != null
|
final txEstimate = envelope != null
|
||||||
? estimateVoiceTransmitDuration(
|
? estimateVoiceTransmitDuration(
|
||||||
packetCount: envelope.total,
|
packetCount: isPartialResume ? missing.length : envelope.total,
|
||||||
mode: envelope.mode,
|
mode: envelope.mode,
|
||||||
durationMs: envelope.durationMs,
|
durationMs: estimatedDurationMs ?? envelope.durationMs,
|
||||||
pathLen: effectivePathLen,
|
pathLen: effectivePathLen,
|
||||||
radioBw: radioBw,
|
radioBw: radioBw,
|
||||||
radioSf: radioSf,
|
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() {
|
void _setUnavailable() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_showToast(AppLocalizations.of(context)!.voiceUnavailable);
|
_showToast(AppLocalizations.of(context)!.voiceUnavailable);
|
||||||
|
|||||||
Reference in New Issue
Block a user