refactor(social): one shared relay connection per identity

Every feature (offers, messaging, trust, profile, the inbox listener) used
to open its OWN RelayPool via social.openSession — several sockets to the
same relays, more battery, and reconnection logic living only in the inbox
listener.

Add SocialConnection: ONE shared session per identity, lazily connected and
reused by all. It watches connectivity — dropping the session when the
network goes and reconnecting when it returns, announcing each change on a
sessions stream so the inbox listener re-subscribes automatically. Callers
get the session via connection.session() and never close it (the connection
owns its lifecycle); recreated/disposed on an identity switch.

- InboxService now consumes the shared connection (subscribes on its
  sessions stream) instead of owning its own socket + connectivity code.
- createOffersCubit + the chat/market/profile screens use the shared
  connection; dead createMessagesCubit/createTrustCubit factories removed.
- DI registers SocialConnection per identity; Bootstrap starts it (after
  the inbox subscribes); switchSocialAccount disposes + recreates it.

Tests: SocialConnection lifecycle (reuse, concurrent connect, offline drop +
reconnect, unreachable retry) with a fake opener + online stream; inbox test
updated. nostr added as a dev_dependency for the channel fake.
This commit is contained in:
vjrj 2026-07-10 21:41:17 +02:00
parent e2b88b4f26
commit bb4ee2fd89
19 changed files with 431 additions and 280 deletions

View file

@ -10,8 +10,8 @@ import '../di/injector.dart';
import '../i18n/strings.g.dart';
import '../services/message_store.dart';
import '../services/profile_cache.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
import '../services/trust_referents.dart';
import '../services/unread_service.dart';
import '../services/wot_settings.dart';
@ -25,7 +25,7 @@ import 'theme.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({
required this.social,
required this.settings,
required this.connection,
required this.peerPubkey,
this.messageStore,
this.profileCache,
@ -35,7 +35,9 @@ class ChatScreen extends StatefulWidget {
});
final SocialService social;
final SocialSettings settings;
/// The shared relay connection (one per identity), carrying messaging + trust.
final SocialConnection connection;
final String peerPubkey;
/// Optional persistence for chat history (keystore-backed); null in tests.
@ -53,7 +55,6 @@ class ChatScreen extends StatefulWidget {
}
class _ChatScreenState extends State<ChatScreen> {
SocialSession? _session;
MessagesCubit? _messages;
TrustCubit? _trust;
bool _loading = true;
@ -79,25 +80,14 @@ class _ChatScreenState extends State<ChatScreen> {
}
Future<void> _init() async {
// One session for this chat carries both messaging and trust (the shared-
// connection shape). Offline (no relays / unreachable) null transports.
final relays = await widget.settings.relayUrls();
SocialSession? session;
if (relays.isNotEmpty) {
try {
session = await widget.social.openSession(relays);
} catch (_) {
session = null;
}
}
// The shared connection carries both messaging and trust. Offline (not
// connected / unreachable) null transports and the screen degrades.
final session = await widget.connection.session();
final cachedName = await widget.profileCache?.name(widget.peerPubkey);
final referents = await widget.trustReferents?.all() ?? const <String>{};
final wotParams =
await widget.wotSettings?.params() ?? WotParams.duniter;
if (!mounted) {
await session?.close();
return;
}
if (!mounted) return; // shared session is owned by the connection, not us
final self = widget.social.publicKeyHex;
final messages = MessagesCubit(
session?.messages,
@ -114,7 +104,6 @@ class _ChatScreenState extends State<ChatScreen> {
);
unawaited(trust.load());
setState(() {
_session = session;
_messages = messages;
_trust = trust;
_peerName = cachedName;
@ -184,7 +173,7 @@ class _ChatScreenState extends State<ChatScreen> {
unawaited(_unread?.markRead(widget.peerPubkey));
_messages?.close();
_trust?.close();
_session?.close();
// The session belongs to the shared connection don't close it here.
_input.dispose();
super.dispose();
}