fix: reliable first-message send, QR routing, auto-lock recovery
Fixes reported by testers and found in an app review: - First message right after install no longer fails. Before the app finished registering with the relay, the outgoing message could crash on a null delivery address. The profile field is now optional and the send path throws a clear "still connecting, try again shortly" error while registration completes in the background. - The QR code and share link now appear only once the delivery address is ready. Previously a link generated before the first connection lacked routing, so the other side could not send the first message. The screen now shows a connecting state and reactively swaps in the QR when ready. - Incomplete contact links are rejected on import instead of failing at first send, with a clear prompt to ask for a fresh link. - Auto-lock no longer silences the app: after locking and unlocking, messaging is restarted (it previously stayed dead until app restart). - Group messages are marked failed on a non-per-member send error instead of hanging in the "sending" state forever. - Hardened the relay client against a rare duplicate connection when toggling Tor or locking mid-connect, and fromShareString now rejects a delivery address of the wrong length. - Pin unifiedpush below 6.2.0 to drop the webcrypto dependency (its native framework lacks LC_ENCRYPTION_INFO and blocked the iOS upload). - Skip Android release lint (network dependency check hangs on the build host and adds no value for a Flutter app).
This commit is contained in:
@@ -26,6 +26,24 @@ class RelayUnavailableError extends SepotUserError {
|
||||
String toString() => 'relay unavailable';
|
||||
}
|
||||
|
||||
/// Vlastní registrace na relay ještě nedoběhla — nemáme sender adresu.
|
||||
/// Přechodný stav po čerstvé instalaci; registrace běží na pozadí.
|
||||
class NotYetRegisteredError extends SepotUserError {
|
||||
const NotYetRegisteredError();
|
||||
|
||||
@override
|
||||
String toString() => 'not registered on relay yet';
|
||||
}
|
||||
|
||||
/// Odkaz/QR kontaktu je neúplný (chybí routing aid+da) — vygenerovaný
|
||||
/// dřív, než měla protistrana dokončenou registraci na relay.
|
||||
class ContactLinkIncompleteError extends SepotUserError {
|
||||
const ContactLinkIncompleteError();
|
||||
|
||||
@override
|
||||
String toString() => 'contact link is missing routing (incomplete)';
|
||||
}
|
||||
|
||||
/// Příloha přesahuje serverový limit.
|
||||
class AttachmentTooLargeError extends SepotUserError {
|
||||
const AttachmentTooLargeError(this.maxBytes);
|
||||
|
||||
@@ -11,6 +11,8 @@ import 'package:sepot/l10n/generated/app_localizations.dart';
|
||||
String describeError(AppLocalizations l10n, Object error) => switch (error) {
|
||||
TrustRootChangedError() => l10n.errorTrustRootChanged,
|
||||
RelayUnavailableError() => l10n.errorRelayUnavailable,
|
||||
NotYetRegisteredError() => l10n.errorNotYetRegistered,
|
||||
ContactLinkIncompleteError() => l10n.errorContactLinkIncomplete,
|
||||
AttachmentTooLargeError(:final maxMb) =>
|
||||
l10n.errorAttachmentTooLarge(maxMb),
|
||||
GroupNeedsMemberError() => l10n.errorGroupNeedsMember,
|
||||
|
||||
@@ -75,6 +75,9 @@ class RelayClient {
|
||||
|
||||
void _setState(RelayConnectionState s) {
|
||||
_state = s;
|
||||
// Po dispose() je controller zavřený — pozdní přechod z doběhnutého
|
||||
// _connect (zombie) by jinak hodil StateError do listenerů.
|
||||
if (_stateController.isClosed) return;
|
||||
_stateController.add(s);
|
||||
}
|
||||
|
||||
@@ -292,6 +295,14 @@ class RelayClient {
|
||||
try {
|
||||
final channel = _transport.connectWs(Uri.parse('$_wsUrl/v1/ws'));
|
||||
await channel.ready;
|
||||
// Během `ready` (přes Tor může viset dlouho) mohl přijít stop()/dispose()
|
||||
// — Tor toggle nebo auto-lock. Bez téhle kontroly by vzniklo zombie
|
||||
// autentizované spojení (relay má 2× jedno zařízení, DELIVERy padají do
|
||||
// zavřených controllerů). Nezakládat ho, jen zavřít.
|
||||
if (!_shouldRun) {
|
||||
await channel.sink.close();
|
||||
return;
|
||||
}
|
||||
_channel = channel;
|
||||
|
||||
// AUTH: výzva-odpověď podepsaná identity klíčem (XEdDSA)
|
||||
@@ -322,9 +333,13 @@ class RelayClient {
|
||||
return;
|
||||
}
|
||||
if (frame.hasDeliver()) {
|
||||
_deliverController.add(frame.deliver);
|
||||
if (!_deliverController.isClosed) {
|
||||
_deliverController.add(frame.deliver);
|
||||
}
|
||||
} else if (frame.hasReceipt()) {
|
||||
_receiptController.add(frame.receipt);
|
||||
if (!_receiptController.isClosed) {
|
||||
_receiptController.add(frame.receipt);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDone: _onDisconnected,
|
||||
|
||||
@@ -496,10 +496,14 @@ class MessagingService {
|
||||
|
||||
Map<String, dynamic> _profile() {
|
||||
final identity = _identity!;
|
||||
final da = myDeliveryAddress;
|
||||
return {
|
||||
'n': identity.displayName,
|
||||
'aid': accountId,
|
||||
'da': base64Encode(myDeliveryAddress!),
|
||||
'aid': ?accountId,
|
||||
// Do doběhnutí vlastní registrace na relay ještě routing nemáme —
|
||||
// pole vynecháme (protistrana ho doplní z pozdějšího profilu), NIKDY
|
||||
// `da!` (null-check pád = „Null check operator used on a null value").
|
||||
'da': ?(da == null ? null : base64Encode(da)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -694,21 +698,30 @@ class MessagingService {
|
||||
await storage.addMessage(group.id, message);
|
||||
onUpdate?.call();
|
||||
|
||||
final anyOk = await _fanoutToGroup(group, {
|
||||
'v': 1,
|
||||
't': 'txt',
|
||||
'id': message.id,
|
||||
'gid': group.id,
|
||||
'txt': text,
|
||||
're': ?replyToId,
|
||||
'ttl': ?ttl,
|
||||
'p': _profile(),
|
||||
});
|
||||
final sent = message.copyWith(
|
||||
status: anyOk ? MessageStatus.sent : MessageStatus.failed);
|
||||
await storage.updateMessage(group.id, sent);
|
||||
onUpdate?.call();
|
||||
return sent;
|
||||
try {
|
||||
final anyOk = await _fanoutToGroup(group, {
|
||||
'v': 1,
|
||||
't': 'txt',
|
||||
'id': message.id,
|
||||
'gid': group.id,
|
||||
'txt': text,
|
||||
're': ?replyToId,
|
||||
'ttl': ?ttl,
|
||||
'p': _profile(),
|
||||
});
|
||||
final sent = message.copyWith(
|
||||
status: anyOk ? MessageStatus.sent : MessageStatus.failed);
|
||||
await storage.updateMessage(group.id, sent);
|
||||
onUpdate?.call();
|
||||
return sent;
|
||||
} catch (e) {
|
||||
// Výjimka mimo per-member cestu (např. build obsahu) by jinak nechala
|
||||
// zprávu navždy ve stavu „odesílá se" (⏱). Označit jako selhalou.
|
||||
await storage.updateMessage(
|
||||
group.id, message.copyWith(status: MessageStatus.failed));
|
||||
onUpdate?.call();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Odešle přílohu do skupiny — blob se šifruje a nahrává JEDNOU,
|
||||
@@ -741,21 +754,28 @@ class MessagingService {
|
||||
await storage.addMessage(group.id, message);
|
||||
onUpdate?.call();
|
||||
|
||||
final anyOk = await _fanoutToGroup(group, {
|
||||
'v': 1,
|
||||
't': 'att',
|
||||
'id': message.id,
|
||||
'gid': group.id,
|
||||
if (caption != null && caption.isNotEmpty) 'txt': caption,
|
||||
'ttl': ?ttl,
|
||||
'att': meta.toWire(),
|
||||
'p': _profile(),
|
||||
});
|
||||
final sent = message.copyWith(
|
||||
status: anyOk ? MessageStatus.sent : MessageStatus.failed);
|
||||
await storage.updateMessage(group.id, sent);
|
||||
onUpdate?.call();
|
||||
return sent;
|
||||
try {
|
||||
final anyOk = await _fanoutToGroup(group, {
|
||||
'v': 1,
|
||||
't': 'att',
|
||||
'id': message.id,
|
||||
'gid': group.id,
|
||||
if (caption != null && caption.isNotEmpty) 'txt': caption,
|
||||
'ttl': ?ttl,
|
||||
'att': meta.toWire(),
|
||||
'p': _profile(),
|
||||
});
|
||||
final sent = message.copyWith(
|
||||
status: anyOk ? MessageStatus.sent : MessageStatus.failed);
|
||||
await storage.updateMessage(group.id, sent);
|
||||
onUpdate?.call();
|
||||
return sent;
|
||||
} catch (e) {
|
||||
await storage.updateMessage(
|
||||
group.id, message.copyWith(status: MessageStatus.failed));
|
||||
onUpdate?.call();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reakce ve skupině (emoji == null = odebrat).
|
||||
@@ -1009,6 +1029,13 @@ class MessagingService {
|
||||
final crypto = _crypto ?? (throw StateError('Service not started'));
|
||||
final relay = _relay!;
|
||||
|
||||
// Vlastní registrace na relay ještě nedoběhla (čerstvá instalace,
|
||||
// pomalá síť, Tor bez Orbotu) → nemáme sender adresu ani sender cert.
|
||||
// Srozumitelná chyba místo null-check pádu; registrace běží na pozadí.
|
||||
if (accountId == null || myDeliveryAddress == null) {
|
||||
throw const NotYetRegisteredError();
|
||||
}
|
||||
|
||||
final peer = contact.publicIdentity;
|
||||
final peerAid = peer.accountId;
|
||||
final peerDa = peer.deliveryAddress;
|
||||
|
||||
Reference in New Issue
Block a user