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.
188 lines
5.9 KiB
Dart
188 lines
5.9 KiB
Dart
import 'package:commons_core/commons_core.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
/// Where a peer stands, from strongest to weakest signal.
|
|
enum TrustTier {
|
|
/// Passes the full Duniter membership rule (sigQty certifications from members,
|
|
/// within stepMax of a bootstrap referent).
|
|
networkMember,
|
|
|
|
/// In your personal circle (you vouch, or a friend-of-a-friend does).
|
|
inYourCircle,
|
|
|
|
/// Vouched for by someone, but not (yet) a member nor in your circle.
|
|
vouched,
|
|
|
|
/// No certifications seen.
|
|
unknown,
|
|
}
|
|
|
|
/// Trust standing of one peer: the full Duniter membership verdict, your own
|
|
/// circle, and the raw certifier count — computed from the public certification
|
|
/// graph with the active [WotParams] and bootstrap referents.
|
|
class TrustState extends Equatable {
|
|
const TrustState({
|
|
this.certifierCount = 0,
|
|
this.iVouch = false,
|
|
this.knownToYou = false,
|
|
this.isNetworkMember = false,
|
|
this.networkBootstrapped = false,
|
|
this.loading = true,
|
|
this.busy = false,
|
|
});
|
|
|
|
/// How many people (network-wide) currently vouch for the peer.
|
|
final int certifierCount;
|
|
|
|
/// Whether this user vouches for the peer.
|
|
final bool iVouch;
|
|
|
|
/// Whether the peer is within the user's own circle (you, or a
|
|
/// friend-of-a-friend, vouch). Spam-resistant and works from day one.
|
|
final bool knownToYou;
|
|
|
|
/// Whether the peer is a full member per the Duniter rule (sigQty + stepMax
|
|
/// from the bootstrap referents).
|
|
final bool isNetworkMember;
|
|
|
|
/// Whether any bootstrap referents exist at all. When false, network
|
|
/// membership can't be determined yet (the trust net isn't seeded) and the UI
|
|
/// should say so instead of implying the peer failed the rule.
|
|
final bool networkBootstrapped;
|
|
|
|
final bool loading;
|
|
final bool busy;
|
|
|
|
/// The strongest applicable signal, for the badge.
|
|
TrustTier get tier {
|
|
if (isNetworkMember) return TrustTier.networkMember;
|
|
if (knownToYou) return TrustTier.inYourCircle;
|
|
if (certifierCount > 0) return TrustTier.vouched;
|
|
return TrustTier.unknown;
|
|
}
|
|
|
|
TrustState copyWith({
|
|
int? certifierCount,
|
|
bool? iVouch,
|
|
bool? knownToYou,
|
|
bool? isNetworkMember,
|
|
bool? networkBootstrapped,
|
|
bool? loading,
|
|
bool? busy,
|
|
}) =>
|
|
TrustState(
|
|
certifierCount: certifierCount ?? this.certifierCount,
|
|
iVouch: iVouch ?? this.iVouch,
|
|
knownToYou: knownToYou ?? this.knownToYou,
|
|
isNetworkMember: isNetworkMember ?? this.isNetworkMember,
|
|
networkBootstrapped: networkBootstrapped ?? this.networkBootstrapped,
|
|
loading: loading ?? this.loading,
|
|
busy: busy ?? this.busy,
|
|
);
|
|
|
|
@override
|
|
List<Object?> get props => [
|
|
certifierCount,
|
|
iVouch,
|
|
knownToYou,
|
|
isNetworkMember,
|
|
networkBootstrapped,
|
|
loading,
|
|
busy,
|
|
];
|
|
}
|
|
|
|
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport],
|
|
/// and computes the full Duniter membership verdict against the bootstrap
|
|
/// [referents] and active [params].
|
|
class TrustCubit extends Cubit<TrustState> {
|
|
TrustCubit(
|
|
this._transport, {
|
|
required this.peerPubkey,
|
|
required this.selfPubkey,
|
|
this.referents = const {},
|
|
this.params = WotParams.duniter,
|
|
Future<void> Function()? onDispose,
|
|
}) : _onDispose = onDispose,
|
|
super(const TrustState());
|
|
|
|
final TrustTransport? _transport;
|
|
final String peerPubkey;
|
|
final String selfPubkey;
|
|
|
|
/// Bootstrap referents (Duniter seeds) the membership rule trusts by
|
|
/// construction. Empty = the network isn't seeded yet.
|
|
final Set<String> referents;
|
|
|
|
/// Active web-of-trust parameters (sigQty / stepMax / validity).
|
|
final WotParams params;
|
|
final Future<void> Function()? _onDispose;
|
|
|
|
bool get isOnline => _transport != null;
|
|
|
|
/// Personal "circle" rule: one vouch from your side, out to a friend-of-a-
|
|
/// friend. Loose by design — "people near you", separate from formal
|
|
/// membership.
|
|
static const _circleThreshold = 1;
|
|
static const _circleDistance = 2;
|
|
|
|
/// Loads the certification graph and computes: the peer's certifier count,
|
|
/// whether you vouch, whether they're in your circle, and whether they pass
|
|
/// the full Duniter membership rule against the bootstrap referents.
|
|
Future<void> load() async {
|
|
final transport = _transport;
|
|
if (transport == null) {
|
|
emit(state.copyWith(loading: false));
|
|
return;
|
|
}
|
|
emit(state.copyWith(loading: true));
|
|
final wot = WebOfTrust.fromCertifications(
|
|
await transport.allCertifications(),
|
|
now: DateTime.now(),
|
|
);
|
|
final certifiers = wot.certifiersOf(peerPubkey);
|
|
final circle = wot.members(
|
|
seeds: {selfPubkey},
|
|
threshold: _circleThreshold,
|
|
maxDistance: _circleDistance,
|
|
);
|
|
// Full membership: only meaningful once the trust net has referents.
|
|
final members = referents.isEmpty
|
|
? const <String>{}
|
|
: wot.membersWith(seeds: referents, params: params);
|
|
emit(state.copyWith(
|
|
certifierCount: certifiers.length,
|
|
iVouch: certifiers.contains(selfPubkey),
|
|
knownToYou: circle.contains(peerPubkey),
|
|
isNetworkMember: members.contains(peerPubkey),
|
|
networkBootstrapped: referents.isNotEmpty,
|
|
loading: false,
|
|
));
|
|
}
|
|
|
|
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
|
|
/// The certification is issued with the active validity so it expires and
|
|
/// must be renewed (Duniter rule).
|
|
Future<void> toggleVouch() async {
|
|
final transport = _transport;
|
|
if (transport == null || peerPubkey == selfPubkey) return;
|
|
emit(state.copyWith(busy: true));
|
|
if (state.iVouch) {
|
|
await transport.revoke(subjectPubkey: peerPubkey);
|
|
} else {
|
|
await transport.certify(
|
|
subjectPubkey: peerPubkey,
|
|
validity: params.sigValidity,
|
|
);
|
|
}
|
|
await load();
|
|
emit(state.copyWith(busy: false));
|
|
}
|
|
|
|
@override
|
|
Future<void> close() async {
|
|
await _onDispose?.call();
|
|
return super.close();
|
|
}
|
|
}
|