Files
sepot/app/lib/providers/messaging_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

68 lines
3.0 KiB
Dart

import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/core/network/relay_client.dart';
import 'package:sepot/core/push/push_notifier.dart';
import 'package:sepot/core/services/messaging_service.dart';
import 'package:sepot/providers/contacts_provider.dart';
import 'package:sepot/providers/conversations_provider.dart';
import 'package:sepot/providers/groups_provider.dart';
import 'package:sepot/providers/identity_provider.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Jediná instance messaging služby (krypto + relay + storage).
final messagingServiceProvider = Provider<MessagingService>((ref) {
final service = MessagingService(storage: ref.read(storageProvider));
ref.onDispose(service.stop);
return service;
});
/// Nastartuje službu, jakmile existuje identita. Watchovat z první
/// obrazovky po odemčení (chat list) — do té doby se nic nespojuje.
/// Předpokládá odemčený storage — na /chats se jde vždy až po unlocku.
final messagingStartProvider = FutureProvider<void>((ref) async {
final identity = await ref.watch(identityProvider.future);
if (identity == null) return;
// Po auto-locku se tenhle provider eager-rebuildne (drží ho pushInitProvider)
// ještě NAD zamčeným storage → service.start() by spadl na „Storage is
// locked" a provider by uvázl v AsyncError (žádný příjem/odeslání až do
// killu procesu). Nad zamčeným storage tiše nic nestartuj; po odemčení
// se provider invaliduje (viz unlock cesty) a spustí start znovu.
final storage = ref.read(storageProvider);
if (!storage.isUnlocked) return;
final service = ref.read(messagingServiceProvider);
if (service.isStarted) return;
service.onUpdate = () {
ref.invalidate(conversationsProvider);
// celá family — jinak se otevřená konverzace nedozví o nové zprávě
// ani o změně stavu přílohy
ref.invalidate(conversationProvider);
ref.invalidate(contactsProvider);
// příchozí ginv/gleave/gmeta mění skupiny
ref.invalidate(groupsProvider);
};
// Appka běží, ale není v popředí (jiná appka / zhasnutý displej se
// živým procesem) → systémová notifikace. Jen jméno, žádný obsah.
// Ztlumená konverzace notifikaci potlačí (unread badge běží dál).
service.onMessageReceived = (contact, message) async {
final visible = WidgetsBinding.instance.lifecycleState ==
AppLifecycleState.resumed;
if (visible) return;
final conv = await ref.read(storageProvider).loadConversation(contact.id);
if (conv?.isMuted == true) return;
PushNotifier.showMessageNotification(contact.displayName);
};
await service.start(identity);
});
/// Stav připojení k relayi pro UI indikátor.
final relayConnectionProvider = StreamProvider<RelayConnectionState>((ref) {
ref.watch(messagingStartProvider);
final service = ref.read(messagingServiceProvider);
return service.connectionStream ?? const Stream.empty();
});