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 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 { 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 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 {}; 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(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 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 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)); } }