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

@ -7,8 +7,8 @@ import '../i18n/strings.g.dart';
import '../services/profile_cache.dart' show shortPubkey;
import '../services/social_account_store.dart';
import '../services/profile_store.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
import 'qr_view.dart';
import 'restart_widget.dart';
import 'theme.dart';
@ -19,7 +19,7 @@ import 'theme.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({
required this.social,
required this.settings,
required this.connection,
required this.profileStore,
required this.accounts,
this.trustNetworkEnabled = false,
@ -27,7 +27,9 @@ class ProfileScreen extends StatefulWidget {
});
final SocialService social;
final SocialSettings settings;
/// The shared relay connection, for publishing your profile.
final SocialConnection connection;
final ProfileStore profileStore;
final SocialAccountStore accounts;
@ -121,20 +123,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
await widget.profileStore
.save(name: _name.text, about: _about.text, g1: _g1.text);
// Best-effort publish to the network so peers see the name.
final relays = await widget.settings.relayUrls();
if (relays.isNotEmpty) {
try {
final session = await widget.social.openSession(relays);
await session.profile.publish(
name: _name.text.trim(),
about: _about.text.trim(),
g1: _g1.text.trim(),
);
await session.close();
} catch (_) {
// offline / unreachable the local copy is saved regardless.
}
// Best-effort publish over the shared connection so peers see the name.
try {
final session = await widget.connection.session();
await session?.profile.publish(
name: _name.text.trim(),
about: _about.text.trim(),
g1: _g1.text.trim(),
);
} catch (_) {
// offline / unreachable the local copy is saved regardless.
}
if (!mounted) return;
setState(() => _saving = false);