Files
sepot/app/lib/providers/contacts_provider.dart
T
Julius Joska 90a1390716 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).
2026-07-18 19:53:41 +02:00

77 lines
2.8 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/core/errors.dart';
import 'package:sepot/models/contact.dart';
import 'package:sepot/models/identity.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Stav seznamu kontaktů
class ContactsNotifier extends AsyncNotifier<List<Contact>> {
@override
Future<List<Contact>> build() async {
final storage = ref.read(storageProvider);
return storage.loadContacts();
}
/// Přidej kontakt z veřejné identity
Future<Contact> addContact(PublicIdentity identity, {bool verified = false}) async {
final storage = ref.read(storageProvider);
final contacts = await future;
// Odkaz bez routingu (aid+da) = protistrana ho vytvořila dřív, než měla
// hotovou registraci. Přidat takový kontakt = časovaná bomba: první
// odeslání spadne na „nemá doručovací adresu". Odmítnout hned s
// jasnou hláškou. (Nullable routing má smysl jen na interních příjmových
// cestách — roster/_findOrCreateContact —, ne při ručním importu.)
if (identity.accountId == null || identity.deliveryAddress == null) {
throw const ContactLinkIncompleteError();
}
// Kontrola duplicity
if (contacts.any((c) => c.publicIdentity.id == identity.id)) {
throw const ContactAlreadyExistsError();
}
final contact = Contact.fromPublicIdentity(identity, verified: verified);
await storage.saveContact(contact);
state = AsyncData([...contacts, contact]);
return contact;
}
/// Smaž kontakt
Future<void> removeContact(String contactId) async {
final storage = ref.read(storageProvider);
await storage.deleteContact(contactId);
final contacts = await future;
state = AsyncData(contacts.where((c) => c.id != contactId).toList());
}
// Pozn.: dřívější verifyContact() (slepé přepnutí flagu) odstraněna —
// ověření jde VÝHRADNĚ přes safety number (ContactVerifyScreen).
/// Zablokuj / odblokuj kontakt. Blokovaný: příchozí zprávy se zahodí,
/// odesílání je v UI zakázané.
Future<void> setBlocked(String contactId, bool blocked) async {
final storage = ref.read(storageProvider);
final contacts = await future;
final contact = contacts.where((c) => c.id == contactId).firstOrNull;
if (contact == null) return;
contact.blocked = blocked;
await storage.saveContact(contact);
state = AsyncData([...contacts]);
}
/// Najdi kontakt podle identity ID
Contact? findByIdentityId(String identityId) {
final contacts = state.valueOrNull;
if (contacts == null) return null;
return contacts.where((c) => c.publicIdentity.id == identityId).firstOrNull;
}
}
final contactsProvider =
AsyncNotifierProvider<ContactsNotifier, List<Contact>>(
ContactsNotifier.new,
);