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
162
apps/app_seeds/test/state/peer_rating_cubit_test.dart
Normal file
162
apps/app_seeds/test/state/peer_rating_cubit_test.dart
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/state/peer_rating_cubit.dart';
|
||||
|
||||
/// In-memory [RatingTransport]: rate/retract act as this device's identity,
|
||||
/// one live rating per (rater, subject) like the Nostr transport.
|
||||
class FakeRatingTransport implements RatingTransport {
|
||||
FakeRatingTransport(this.selfId);
|
||||
final String selfId;
|
||||
final Map<String, Map<String, Rating>> _bySubject = {}; // subject -> rater
|
||||
|
||||
/// Test helper: record an arbitrary rater→subject rating.
|
||||
void addRating(String rater, String subject, int stars,
|
||||
{String comment = ''}) =>
|
||||
(_bySubject[subject] ??= {})[rater] = Rating(
|
||||
rater: rater,
|
||||
subject: subject,
|
||||
stars: stars,
|
||||
ratedAt: DateTime(2026),
|
||||
comment: comment,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> rate({
|
||||
required String subjectPubkey,
|
||||
required int stars,
|
||||
String comment = '',
|
||||
}) async =>
|
||||
addRating(selfId, subjectPubkey, stars, comment: comment);
|
||||
|
||||
@override
|
||||
Future<void> retract({required String subjectPubkey}) async =>
|
||||
_bySubject[subjectPubkey]?.remove(selfId);
|
||||
|
||||
@override
|
||||
Future<List<Rating>> ratingsOf(String subjectPubkey) async =>
|
||||
List.of(_bySubject[subjectPubkey]?.values ?? const <Rating>[]);
|
||||
|
||||
@override
|
||||
Future<Rating?> myRatingOf(String subjectPubkey) async =>
|
||||
_bySubject[subjectPubkey]?[selfId];
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
/// Minimal trust transport exposing a fixed certification graph, for the
|
||||
/// circle weighting.
|
||||
class FakeTrustTransport implements TrustTransport {
|
||||
final List<Certification> certs = [];
|
||||
|
||||
void addCert(String issuer, String subject) => certs.add(Certification(
|
||||
issuer: issuer,
|
||||
subject: subject,
|
||||
issuedAt: DateTime(2026),
|
||||
));
|
||||
|
||||
@override
|
||||
Future<List<Certification>> allCertifications() async => List.of(certs);
|
||||
|
||||
@override
|
||||
Future<Set<String>> certifiersOf(String subjectPubkey) async => {
|
||||
for (final c in certs)
|
||||
if (c.subject == subjectPubkey) c.issuer,
|
||||
};
|
||||
|
||||
@override
|
||||
Future<void> certify({
|
||||
required String subjectPubkey,
|
||||
Duration validity = const Duration(days: 365),
|
||||
String note = '',
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> revoke({required String subjectPubkey}) async {}
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
const me = 'me';
|
||||
const peer = 'peer';
|
||||
|
||||
test('aggregates count and average over active ratings', () async {
|
||||
final ratings = FakeRatingTransport(me)
|
||||
..addRating('a', peer, 5)
|
||||
..addRating('b', peer, 2);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
expect(cubit.state.count, 2);
|
||||
expect(cubit.state.average, closeTo(3.5, 0.001));
|
||||
expect(cubit.state.circleCount, 0); // no trust graph wired
|
||||
expect(cubit.state.iRated, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('counts ratings coming from your circle', () async {
|
||||
final ratings = FakeRatingTransport(me)
|
||||
..addRating('friend', peer, 5) // you vouch for 'friend'
|
||||
..addRating('stranger', peer, 1);
|
||||
final trust = FakeTrustTransport()..addCert(me, 'friend');
|
||||
final cubit = PeerRatingCubit(
|
||||
ratings,
|
||||
peerPubkey: peer,
|
||||
selfPubkey: me,
|
||||
trust: trust,
|
||||
);
|
||||
await cubit.load();
|
||||
expect(cubit.state.count, 2);
|
||||
expect(cubit.state.circleCount, 1);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('rate publishes then reloads; re-rating replaces', () async {
|
||||
final ratings = FakeRatingTransport(me);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
|
||||
await cubit.rate(4, comment: 'Nice seeds');
|
||||
expect(cubit.state.myStars, 4);
|
||||
expect(cubit.state.myComment, 'Nice seeds');
|
||||
expect(cubit.state.count, 1);
|
||||
|
||||
await cubit.rate(2);
|
||||
expect(cubit.state.myStars, 2);
|
||||
expect(cubit.state.count, 1); // replaced, not piled up
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('retract removes your rating', () async {
|
||||
final ratings = FakeRatingTransport(me)..addRating(me, peer, 3);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
expect(cubit.state.iRated, isTrue);
|
||||
|
||||
await cubit.retract();
|
||||
expect(cubit.state.iRated, isFalse);
|
||||
expect(cubit.state.count, 0);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('never rates self', () async {
|
||||
final ratings = FakeRatingTransport(me);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: me, selfPubkey: me);
|
||||
await cubit.load();
|
||||
await cubit.rate(5);
|
||||
expect(cubit.state.iRated, isFalse);
|
||||
expect(cubit.state.count, 0);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('offline (no transport) never throws', () async {
|
||||
final cubit = PeerRatingCubit(null, peerPubkey: peer, selfPubkey: me);
|
||||
expect(cubit.isOnline, isFalse);
|
||||
await cubit.load();
|
||||
await cubit.rate(5);
|
||||
await cubit.retract();
|
||||
expect(cubit.state.loading, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue