feat(ratings): Wallapop-style ratings in chat and offer detail
- SocialSession grows a ratings transport (fifth interface on the one shared channel). - PeerRatingCubit: count, locale-aware average, and circleCount — how many ratings come from your own circle (reuses the ego-centric trust rule), the signal that makes strangers' review-stuffing irrelevant. - Chat: slim rating strip under the trust banner; you can rate only someone you've talked with (soft anchor until the signed bilateral exchange form lands). Sheet with 5 stars + optional comment, pre-filled for editing, with a take-back action. - Offer detail: author's stars under their name, with 'N from people you know' when your circle rated them; nothing shown when unrated. - i18n ratings.* (en/es/pt/ast); tests for cubit and sheet.
This commit is contained in:
parent
acdacf1821
commit
1926e3bd98
18 changed files with 873 additions and 12 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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue