feat(block2): social settings + offers cubit factory (area, relays)

Persistence + wiring the offers screen needs, per the product decisions:
- SocialSettings (keystore-backed, no plaintext): coarse area geohash +
  configurable relay URLs, both empty by default (no bundled public relays —
  don't leak metadata to third parties). Registered in DI.
- createOffersCubit(): opens a SocialSession on the first configured relay when
  online, else a null-transport cubit that degrades gracefully. OffersCubit now
  closes the owning session on dispose.

4 settings tests green; offers/social suites still green.
This commit is contained in:
vjrj 2026-07-10 02:54:06 +02:00
parent 7fd87e6e62
commit 528b499209
4 changed files with 116 additions and 1 deletions

View file

@ -4,6 +4,9 @@ import 'package:commons_core/commons_core.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
/// State of the offer discovery/publish screen. Transport-agnostic it holds
/// only what the UI shows, never a relay handle.
class OffersState extends Equatable {
@ -60,9 +63,15 @@ class OffersState extends Equatable {
/// UI stays offline-tolerant (a null transport = the social layer is unavailable
/// and the screen degrades gracefully).
class OffersCubit extends Cubit<OffersState> {
OffersCubit(this._transport) : super(const OffersState());
OffersCubit(this._transport, {Future<void> Function()? onDispose})
: _onDispose = onDispose,
super(const OffersState());
final OfferTransport? _transport;
/// Closes the owning [SocialSession]/connection when the cubit is disposed
/// (null in tests, where the transport is a fake with nothing to close).
final Future<void> Function()? _onDispose;
StreamSubscription<Offer>? _sub;
/// Whether a live transport is available (relay configured and reachable).
@ -113,6 +122,24 @@ class OffersCubit extends Cubit<OffersState> {
@override
Future<void> close() async {
await _sub?.cancel();
await _onDispose?.call();
return super.close();
}
}
/// Opens an [OffersCubit] wired to the social layer, or an offline one that
/// degrades gracefully. Local-first: when no relay is configured (or connecting
/// fails), the cubit is created with a null transport and the screen still opens.
Future<OffersCubit> createOffersCubit(
SocialService social,
SocialSettings settings,
) async {
final relays = await settings.relayUrls();
if (relays.isEmpty) return OffersCubit(null);
try {
final session = await social.openSession(relays.first);
return OffersCubit(session.offers, onDispose: session.close);
} catch (_) {
return OffersCubit(null); // offline / relay unreachable
}
}