Merge branch 'claude/festive-einstein-384c45'
Ego-centric trust replaces the global Duniter WoT, plus Wallapop-style ratings v1 (kind 30778). Conflicts resolved: pubspec assets (kept seed_saving, dropped trust referents), chat_screen (avatars/no-links from main + rating strip and simplified TrustCubit from the branch), strings.g.dart regenerated from merged sources.
This commit is contained in:
commit
456cc8154d
40 changed files with 1777 additions and 876 deletions
145
apps/app_seeds/lib/state/peer_rating_cubit.dart
Normal file
145
apps/app_seeds/lib/state/peer_rating_cubit.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Reputation of one peer, seen from this user's position: everyone's active
|
||||
/// ratings plus how many of those come from people in your own circle — the
|
||||
/// signal that makes strangers' review-stuffing irrelevant.
|
||||
class PeerRatingState extends Equatable {
|
||||
const PeerRatingState({
|
||||
this.count = 0,
|
||||
this.average = 0,
|
||||
this.circleCount = 0,
|
||||
this.myStars,
|
||||
this.myComment = '',
|
||||
this.loading = true,
|
||||
this.busy = false,
|
||||
});
|
||||
|
||||
/// Active ratings of the peer, one per rater.
|
||||
final int count;
|
||||
|
||||
/// Mean stars over [count]; 0 when there are none.
|
||||
final double average;
|
||||
|
||||
/// How many of those ratings come from your circle (you or a
|
||||
/// friend-of-a-friend). These are the ones worth highlighting.
|
||||
final int circleCount;
|
||||
|
||||
/// Your own rating of the peer, if any.
|
||||
final int? myStars;
|
||||
final String myComment;
|
||||
|
||||
final bool loading;
|
||||
final bool busy;
|
||||
|
||||
bool get hasRatings => count > 0;
|
||||
bool get iRated => myStars != null;
|
||||
|
||||
PeerRatingState copyWith({
|
||||
int? count,
|
||||
double? average,
|
||||
int? circleCount,
|
||||
int? Function()? myStars,
|
||||
String? myComment,
|
||||
bool? loading,
|
||||
bool? busy,
|
||||
}) =>
|
||||
PeerRatingState(
|
||||
count: count ?? this.count,
|
||||
average: average ?? this.average,
|
||||
circleCount: circleCount ?? this.circleCount,
|
||||
myStars: myStars != null ? myStars() : this.myStars,
|
||||
myComment: myComment ?? this.myComment,
|
||||
loading: loading ?? this.loading,
|
||||
busy: busy ?? this.busy,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[count, average, circleCount, myStars, myComment, loading, busy];
|
||||
}
|
||||
|
||||
/// Reads and edits this user's rating of [peerPubkey] over a
|
||||
/// [RatingTransport], and summarises everyone else's. The circle weighting
|
||||
/// reuses the trust graph (same ego-centric rule as TrustCubit).
|
||||
class PeerRatingCubit extends Cubit<PeerRatingState> {
|
||||
PeerRatingCubit(
|
||||
this._ratings, {
|
||||
required this.peerPubkey,
|
||||
required this.selfPubkey,
|
||||
TrustTransport? trust,
|
||||
}) : _trust = trust,
|
||||
super(const PeerRatingState());
|
||||
|
||||
final RatingTransport? _ratings;
|
||||
final TrustTransport? _trust;
|
||||
final String peerPubkey;
|
||||
final String selfPubkey;
|
||||
|
||||
bool get isOnline => _ratings != null;
|
||||
|
||||
/// Same personal "circle" rule as TrustCubit: one vouch from your side, out
|
||||
/// to a friend-of-a-friend.
|
||||
static const _circleThreshold = 1;
|
||||
static const _circleDistance = 2;
|
||||
|
||||
Future<void> load() async {
|
||||
final ratings = _ratings;
|
||||
if (ratings == null) {
|
||||
emit(state.copyWith(loading: false));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(loading: true));
|
||||
final all = await ratings.ratingsOf(peerPubkey);
|
||||
final mine = await ratings.myRatingOf(peerPubkey);
|
||||
|
||||
var circle = const <String>{};
|
||||
final trust = _trust;
|
||||
if (trust != null && all.isNotEmpty) {
|
||||
final wot = WebOfTrust.fromCertifications(
|
||||
await trust.allCertifications(),
|
||||
now: DateTime.now(),
|
||||
);
|
||||
circle = wot.members(
|
||||
seeds: {selfPubkey},
|
||||
threshold: _circleThreshold,
|
||||
maxDistance: _circleDistance,
|
||||
);
|
||||
}
|
||||
|
||||
final total = all.fold<int>(0, (sum, r) => sum + r.stars);
|
||||
emit(state.copyWith(
|
||||
count: all.length,
|
||||
average: all.isEmpty ? 0 : total / all.length,
|
||||
circleCount: all.where((r) => circle.contains(r.rater)).length,
|
||||
myStars: () => mine?.stars,
|
||||
myComment: mine?.comment ?? '',
|
||||
loading: false,
|
||||
));
|
||||
}
|
||||
|
||||
/// Publishes (or replaces) your rating, then reloads. Never rates self.
|
||||
Future<void> rate(int stars, {String comment = ''}) async {
|
||||
final ratings = _ratings;
|
||||
if (ratings == null || peerPubkey == selfPubkey) return;
|
||||
emit(state.copyWith(busy: true));
|
||||
await ratings.rate(
|
||||
subjectPubkey: peerPubkey,
|
||||
stars: stars,
|
||||
comment: comment,
|
||||
);
|
||||
await load();
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
|
||||
/// Takes your rating back, then reloads.
|
||||
Future<void> retract() async {
|
||||
final ratings = _ratings;
|
||||
if (ratings == null) return;
|
||||
emit(state.copyWith(busy: true));
|
||||
await ratings.retract(subjectPubkey: peerPubkey);
|
||||
await load();
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,32 +2,28 @@ 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.
|
||||
/// Where a peer stands, from strongest to weakest signal. Trust is
|
||||
/// ego-centric: computed from YOUR position in the public vouch graph,
|
||||
/// never from a global membership verdict.
|
||||
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 for by someone, but not (yet) 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.
|
||||
/// Trust standing of one peer, seen from this user's position: your own
|
||||
/// vouch, your circle (friend-of-a-friend), and the raw certifier count —
|
||||
/// computed from the public certification graph.
|
||||
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,
|
||||
});
|
||||
|
|
@ -42,21 +38,11 @@ class TrustState extends Equatable {
|
|||
/// 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;
|
||||
|
|
@ -66,8 +52,6 @@ class TrustState extends Equatable {
|
|||
int? certifierCount,
|
||||
bool? iVouch,
|
||||
bool? knownToYou,
|
||||
bool? isNetworkMember,
|
||||
bool? networkBootstrapped,
|
||||
bool? loading,
|
||||
bool? busy,
|
||||
}) =>
|
||||
|
|
@ -75,8 +59,6 @@ class TrustState extends Equatable {
|
|||
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,
|
||||
);
|
||||
|
|
@ -86,23 +68,19 @@ class TrustState extends Equatable {
|
|||
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].
|
||||
/// Reads and toggles this user's vouch for [peerPubkey] over a
|
||||
/// [TrustTransport], and computes the peer's standing from this user's own
|
||||
/// position in the vouch graph (ego-centric — no referents, no parameters).
|
||||
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());
|
||||
|
|
@ -110,26 +88,20 @@ class TrustCubit extends Cubit<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.
|
||||
/// friend. Loose by design — "people near you".
|
||||
static const _circleThreshold = 1;
|
||||
static const _circleDistance = 2;
|
||||
|
||||
/// Vouches expire and must be renewed, so stale trust prunes itself.
|
||||
static const _vouchValidity = Duration(days: 365);
|
||||
|
||||
/// 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.
|
||||
/// whether you vouch, and whether they're in your circle.
|
||||
Future<void> load() async {
|
||||
final transport = _transport;
|
||||
if (transport == null) {
|
||||
|
|
@ -147,23 +119,17 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
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).
|
||||
/// The certification is issued with a validity so it expires and must be
|
||||
/// renewed.
|
||||
Future<void> toggleVouch() async {
|
||||
final transport = _transport;
|
||||
if (transport == null || peerPubkey == selfPubkey) return;
|
||||
|
|
@ -173,7 +139,7 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
} else {
|
||||
await transport.certify(
|
||||
subjectPubkey: peerPubkey,
|
||||
validity: params.sigValidity,
|
||||
validity: _vouchValidity,
|
||||
);
|
||||
}
|
||||
await load();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue