mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add toast for image fetch failures
This commit is contained in:
@@ -5,22 +5,22 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00023">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.387742">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.428509">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="112.078035">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="102.185635">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="215.877264">
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="3.215591">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
157
lib/utils/transmission_target_resolver.dart
Normal file
157
lib/utils/transmission_target_resolver.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
|
||||
enum TransmissionTargetFailure { unknownContact, unknownRoute, tooFar }
|
||||
|
||||
class TransmissionTargetResolution {
|
||||
final Contact? target;
|
||||
final TransmissionTargetFailure? failure;
|
||||
final int maxHops;
|
||||
|
||||
const TransmissionTargetResolution({
|
||||
required this.target,
|
||||
required this.failure,
|
||||
required this.maxHops,
|
||||
});
|
||||
|
||||
int get hops => target?.outPathLen ?? -1;
|
||||
bool get isValid => target != null && failure == null;
|
||||
}
|
||||
|
||||
class TransmissionTargetResolver {
|
||||
const TransmissionTargetResolver._();
|
||||
|
||||
static Contact? resolveLocalTarget({
|
||||
required ContactsProvider contactsProvider,
|
||||
required bool isSentByMe,
|
||||
Uint8List? recipientPublicKey,
|
||||
Uint8List? senderPublicKeyPrefix,
|
||||
String? senderKey6FromEnvelope,
|
||||
String? senderName,
|
||||
}) {
|
||||
if (isSentByMe) {
|
||||
final recipient = _findByRecipientKey(contactsProvider, recipientPublicKey);
|
||||
if (recipient != null) return recipient;
|
||||
}
|
||||
|
||||
final byEnvelope = _findByEnvelopeKey6(contactsProvider, senderKey6FromEnvelope);
|
||||
if (byEnvelope != null) return byEnvelope;
|
||||
|
||||
final byPrefix = _findByPrefix(contactsProvider, senderPublicKeyPrefix);
|
||||
if (byPrefix != null) return byPrefix;
|
||||
|
||||
return _findByName(contactsProvider, senderName);
|
||||
}
|
||||
|
||||
static Future<TransmissionTargetResolution> resolveFetchTarget({
|
||||
required ContactsProvider contactsProvider,
|
||||
required Future<void> Function() refreshContacts,
|
||||
required bool isSentByMe,
|
||||
Uint8List? recipientPublicKey,
|
||||
Uint8List? senderPublicKeyPrefix,
|
||||
String? senderKey6FromEnvelope,
|
||||
String? senderName,
|
||||
required int maxFetchHops,
|
||||
}) async {
|
||||
var target = resolveLocalTarget(
|
||||
contactsProvider: contactsProvider,
|
||||
isSentByMe: isSentByMe,
|
||||
recipientPublicKey: recipientPublicKey,
|
||||
senderPublicKeyPrefix: senderPublicKeyPrefix,
|
||||
senderKey6FromEnvelope: senderKey6FromEnvelope,
|
||||
senderName: senderName,
|
||||
);
|
||||
|
||||
if (target == null || target.outPathLen < 0 || target.outPathLen > maxFetchHops) {
|
||||
await refreshContacts();
|
||||
target = resolveLocalTarget(
|
||||
contactsProvider: contactsProvider,
|
||||
isSentByMe: isSentByMe,
|
||||
recipientPublicKey: recipientPublicKey,
|
||||
senderPublicKeyPrefix: senderPublicKeyPrefix,
|
||||
senderKey6FromEnvelope: senderKey6FromEnvelope,
|
||||
senderName: senderName,
|
||||
);
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
return TransmissionTargetResolution(
|
||||
target: null,
|
||||
failure: TransmissionTargetFailure.unknownContact,
|
||||
maxHops: maxFetchHops,
|
||||
);
|
||||
}
|
||||
if (target.outPathLen < 0) {
|
||||
return TransmissionTargetResolution(
|
||||
target: target,
|
||||
failure: TransmissionTargetFailure.unknownRoute,
|
||||
maxHops: maxFetchHops,
|
||||
);
|
||||
}
|
||||
if (target.outPathLen > maxFetchHops) {
|
||||
return TransmissionTargetResolution(
|
||||
target: target,
|
||||
failure: TransmissionTargetFailure.tooFar,
|
||||
maxHops: maxFetchHops,
|
||||
);
|
||||
}
|
||||
return TransmissionTargetResolution(
|
||||
target: target,
|
||||
failure: null,
|
||||
maxHops: maxFetchHops,
|
||||
);
|
||||
}
|
||||
|
||||
static Contact? _findByRecipientKey(
|
||||
ContactsProvider contactsProvider,
|
||||
Uint8List? recipientKey,
|
||||
) {
|
||||
if (recipientKey == null || recipientKey.isEmpty) return null;
|
||||
final byKey = contactsProvider.findContactByKey(recipientKey);
|
||||
if (byKey != null) return byKey;
|
||||
if (recipientKey.length >= 6) {
|
||||
return contactsProvider.findContactByPrefix(
|
||||
Uint8List.fromList(recipientKey.sublist(0, 6)),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Contact? _findByEnvelopeKey6(
|
||||
ContactsProvider contactsProvider,
|
||||
String? senderKey6FromEnvelope,
|
||||
) {
|
||||
if (senderKey6FromEnvelope == null || senderKey6FromEnvelope.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return contactsProvider.findContactByPrefixHex(senderKey6FromEnvelope);
|
||||
}
|
||||
|
||||
static Contact? _findByPrefix(
|
||||
ContactsProvider contactsProvider,
|
||||
Uint8List? senderPublicKeyPrefix,
|
||||
) {
|
||||
if (senderPublicKeyPrefix == null || senderPublicKeyPrefix.length < 6) {
|
||||
return null;
|
||||
}
|
||||
return contactsProvider.findContactByPrefix(
|
||||
Uint8List.fromList(senderPublicKeyPrefix.sublist(0, 6)),
|
||||
);
|
||||
}
|
||||
|
||||
static Contact? _findByName(
|
||||
ContactsProvider contactsProvider,
|
||||
String? senderName,
|
||||
) {
|
||||
final normalized = senderName?.trim();
|
||||
if (normalized == null || normalized.isEmpty) return null;
|
||||
for (final contact in contactsProvider.contacts) {
|
||||
if (contact.advName.trim().toLowerCase() == normalized.toLowerCase()) {
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_avif/flutter_avif.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/message.dart';
|
||||
import '../../providers/connection_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../providers/image_provider.dart' as ip;
|
||||
import '../../utils/image_message_parser.dart';
|
||||
import '../../utils/transmission_target_resolver.dart';
|
||||
import 'transfer_timeout.dart';
|
||||
|
||||
/// A message bubble that shows a received or sent image.
|
||||
@@ -58,8 +58,16 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
||||
|
||||
return Consumer<ip.ImageProvider>(
|
||||
builder: (context, imageProvider, _) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final session = imageProvider.session(envelope.sessionId);
|
||||
final sender = _resolveSender(envelope);
|
||||
final sender = TransmissionTargetResolver.resolveLocalTarget(
|
||||
contactsProvider: contactsProvider,
|
||||
isSentByMe: widget.isSentByMe,
|
||||
recipientPublicKey: widget.message.recipientPublicKey,
|
||||
senderPublicKeyPrefix: widget.message.senderPublicKeyPrefix,
|
||||
senderKey6FromEnvelope: envelope.senderKey6,
|
||||
senderName: widget.message.senderName,
|
||||
);
|
||||
final effectivePathLen =
|
||||
sender != null && sender.outPathLen >= 0
|
||||
? sender.outPathLen
|
||||
@@ -213,35 +221,42 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
||||
}) async {
|
||||
if (_isRequesting) return;
|
||||
final conn = context.read<ConnectionProvider>();
|
||||
var sender = _resolveSender(envelope);
|
||||
if (sender == null ||
|
||||
sender.outPathLen < 0 ||
|
||||
sender.outPathLen > _maxFetchHops) {
|
||||
await conn.getContacts();
|
||||
if (!mounted) return;
|
||||
sender = _resolveSender(envelope);
|
||||
}
|
||||
if (sender == null) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final resolution = await TransmissionTargetResolver.resolveFetchTarget(
|
||||
contactsProvider: contactsProvider,
|
||||
refreshContacts: conn.getContacts,
|
||||
isSentByMe: widget.isSentByMe,
|
||||
recipientPublicKey: widget.message.recipientPublicKey,
|
||||
senderPublicKeyPrefix: widget.message.senderPublicKeyPrefix,
|
||||
senderKey6FromEnvelope: envelope.senderKey6,
|
||||
senderName: widget.message.senderName,
|
||||
maxFetchHops: _maxFetchHops,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
if (resolution.failure == TransmissionTargetFailure.unknownContact) {
|
||||
await _showBlockingAlert(
|
||||
'Cannot fetch image',
|
||||
'Sender contact is unknown. Sync contacts first.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sender.outPathLen < 0) {
|
||||
if (resolution.failure == TransmissionTargetFailure.unknownRoute) {
|
||||
await _showBlockingAlert(
|
||||
'Cannot fetch image',
|
||||
'Sender route is unknown. Sync contacts/path first.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sender.outPathLen > _maxFetchHops) {
|
||||
if (resolution.failure == TransmissionTargetFailure.tooFar) {
|
||||
await _showBlockingAlert(
|
||||
'Cannot fetch image',
|
||||
'Message is too far (${sender.outPathLen} hops, max $_maxFetchHops).',
|
||||
'Message is too far (${resolution.hops} hops, max ${resolution.maxHops}).',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final sender = resolution.target!;
|
||||
if (sender.outPathLen >= 2) {
|
||||
_showToast(
|
||||
'Image fetch over ${sender.outPathLen} hops may take a while.',
|
||||
@@ -335,47 +350,6 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
||||
);
|
||||
}
|
||||
|
||||
Contact? _resolveSender(ImageEnvelope envelope) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
|
||||
// For sent direct messages, fetch must target the recipient peer first.
|
||||
final recipientKey = widget.message.recipientPublicKey;
|
||||
if (widget.isSentByMe && recipientKey != null && recipientKey.isNotEmpty) {
|
||||
final byKey = contactsProvider.findContactByKey(recipientKey);
|
||||
if (byKey != null) return byKey;
|
||||
if (recipientKey.length >= 6) {
|
||||
final byPrefix = contactsProvider.findContactByPrefix(
|
||||
Uint8List.fromList(recipientKey.sublist(0, 6)),
|
||||
);
|
||||
if (byPrefix != null) return byPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
final contact = contactsProvider.findContactByPrefixHex(
|
||||
envelope.senderKey6,
|
||||
);
|
||||
if (contact != null) return contact;
|
||||
|
||||
final senderPrefix = widget.message.senderPublicKeyPrefix;
|
||||
if (senderPrefix != null && senderPrefix.length >= 6) {
|
||||
final c = contactsProvider.findContactByPrefix(
|
||||
Uint8List.fromList(senderPrefix.sublist(0, 6)),
|
||||
);
|
||||
if (c != null) return c;
|
||||
}
|
||||
|
||||
final senderName = widget.message.senderName?.trim();
|
||||
if (senderName != null && senderName.isNotEmpty) {
|
||||
for (final c in contactsProvider.contacts) {
|
||||
if (c.advName.trim().toLowerCase() == senderName.toLowerCase()) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _showToast(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/message.dart';
|
||||
import '../../providers/connection_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../providers/voice_provider.dart';
|
||||
import '../../utils/transmission_target_resolver.dart';
|
||||
import '../../utils/voice_message_parser.dart';
|
||||
import 'transfer_timeout.dart';
|
||||
|
||||
@@ -55,9 +54,17 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
|
||||
return Consumer<VoiceProvider>(
|
||||
builder: (context, voiceProvider, _) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final session = voiceProvider.session(voiceId);
|
||||
final envelope = VoiceEnvelope.tryParseText(widget.message.text);
|
||||
final sender = _resolveSenderContact();
|
||||
final sender = TransmissionTargetResolver.resolveLocalTarget(
|
||||
contactsProvider: contactsProvider,
|
||||
isSentByMe: widget.isSentByMe,
|
||||
recipientPublicKey: widget.message.recipientPublicKey,
|
||||
senderPublicKeyPrefix: widget.message.senderPublicKeyPrefix,
|
||||
senderKey6FromEnvelope: envelope?.senderKey6,
|
||||
senderName: widget.message.senderName,
|
||||
);
|
||||
final effectivePathLen =
|
||||
sender != null && sender.outPathLen >= 0
|
||||
? sender.outPathLen
|
||||
@@ -208,35 +215,42 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
}) async {
|
||||
if (_isRequesting) return;
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
var sender = _resolveSenderContact();
|
||||
if (sender == null ||
|
||||
sender.outPathLen < 0 ||
|
||||
sender.outPathLen > _maxFetchHops) {
|
||||
await connectionProvider.getContacts();
|
||||
if (!mounted) return;
|
||||
sender = _resolveSenderContact();
|
||||
}
|
||||
if (sender == null) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final resolution = await TransmissionTargetResolver.resolveFetchTarget(
|
||||
contactsProvider: contactsProvider,
|
||||
refreshContacts: connectionProvider.getContacts,
|
||||
isSentByMe: widget.isSentByMe,
|
||||
recipientPublicKey: widget.message.recipientPublicKey,
|
||||
senderPublicKeyPrefix: widget.message.senderPublicKeyPrefix,
|
||||
senderKey6FromEnvelope: envelope?.senderKey6,
|
||||
senderName: widget.message.senderName,
|
||||
maxFetchHops: _maxFetchHops,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
if (resolution.failure == TransmissionTargetFailure.unknownContact) {
|
||||
await _showBlockingAlert(
|
||||
'Cannot fetch voice',
|
||||
'Sender contact is unknown. Sync contacts first.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sender.outPathLen < 0) {
|
||||
if (resolution.failure == TransmissionTargetFailure.unknownRoute) {
|
||||
await _showBlockingAlert(
|
||||
'Cannot fetch voice',
|
||||
'Sender route is unknown. Sync contacts/path first.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sender.outPathLen > _maxFetchHops) {
|
||||
if (resolution.failure == TransmissionTargetFailure.tooFar) {
|
||||
await _showBlockingAlert(
|
||||
'Cannot fetch voice',
|
||||
'Message is too far (${sender.outPathLen} hops, max $_maxFetchHops).',
|
||||
'Message is too far (${resolution.hops} hops, max ${resolution.maxHops}).',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final sender = resolution.target!;
|
||||
if (sender.outPathLen >= 2) {
|
||||
_showToast(
|
||||
'Voice fetch over ${sender.outPathLen} hops may take a while.',
|
||||
@@ -319,50 +333,6 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
});
|
||||
}
|
||||
|
||||
Contact? _resolveSenderContact() {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
|
||||
// For sent direct messages, fetch must target the recipient peer first.
|
||||
final recipientKey = widget.message.recipientPublicKey;
|
||||
if (widget.isSentByMe && recipientKey != null && recipientKey.isNotEmpty) {
|
||||
final byKey = contactsProvider.findContactByKey(recipientKey);
|
||||
if (byKey != null) return byKey;
|
||||
if (recipientKey.length >= 6) {
|
||||
final byPrefix = contactsProvider.findContactByPrefix(
|
||||
Uint8List.fromList(recipientKey.sublist(0, 6)),
|
||||
);
|
||||
if (byPrefix != null) return byPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
final senderPrefix = widget.message.senderPublicKeyPrefix;
|
||||
if (senderPrefix != null && senderPrefix.length >= 6) {
|
||||
final contact = contactsProvider.findContactByPrefix(
|
||||
Uint8List.fromList(senderPrefix.sublist(0, 6)),
|
||||
);
|
||||
if (contact != null) return contact;
|
||||
}
|
||||
|
||||
final envelope = VoiceEnvelope.tryParseText(widget.message.text);
|
||||
if (envelope != null) {
|
||||
final contact = contactsProvider.findContactByPrefixHex(
|
||||
envelope.senderKey6,
|
||||
);
|
||||
if (contact != null) return contact;
|
||||
}
|
||||
|
||||
final senderName = widget.message.senderName?.trim();
|
||||
if (senderName != null && senderName.isNotEmpty) {
|
||||
for (final contact in contactsProvider.contacts) {
|
||||
if (contact.advName.trim().toLowerCase() == senderName.toLowerCase()) {
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _showToast(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
Reference in New Issue
Block a user