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:
vjrj 2026-07-11 13:25:51 +02:00
commit 456cc8154d
40 changed files with 1777 additions and 876 deletions

View file

@ -15,12 +15,12 @@ import '../services/profile_cache.dart';
import '../services/profile_store.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import '../services/trust_referents.dart';
import '../services/unread_service.dart';
import '../services/wot_settings.dart';
import '../state/messages_cubit.dart';
import '../state/peer_rating_cubit.dart';
import '../state/trust_cubit.dart';
import 'peer_avatar.dart';
import 'rating_sheet.dart';
import 'theme.dart';
/// A 1:1 encrypted chat with one peer (redesign screen 10). Private and
@ -33,8 +33,6 @@ class ChatScreen extends StatefulWidget {
required this.peerPubkey,
this.messageStore,
this.profileCache,
this.trustReferents,
this.wotSettings,
super.key,
});
@ -51,10 +49,6 @@ class ChatScreen extends StatefulWidget {
/// Optional cache of peer display names; null in tests.
final ProfileCache? profileCache;
/// Web-of-trust bootstrap referents + parameters, for the membership verdict.
final TrustReferents? trustReferents;
final WotSettings? wotSettings;
@override
State<ChatScreen> createState() => _ChatScreenState();
}
@ -62,6 +56,7 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
MessagesCubit? _messages;
TrustCubit? _trust;
PeerRatingCubit? _rating;
bool _loading = true;
String? _peerName;
String? _selfName;
@ -95,8 +90,6 @@ class _ChatScreenState extends State<ChatScreen> {
final selfName = getIt.isRegistered<ProfileStore>()
? await getIt<ProfileStore>().name()
: '';
final referents = await widget.trustReferents?.all() ?? const <String>{};
final wotParams = await widget.wotSettings?.params() ?? WotParams.duniter;
if (!mounted) return; // shared session is owned by the connection, not us
final self = widget.social.publicKeyHex;
final messages = MessagesCubit(
@ -109,13 +102,19 @@ class _ChatScreenState extends State<ChatScreen> {
session?.trust,
peerPubkey: widget.peerPubkey,
selfPubkey: self,
referents: referents,
params: wotParams,
);
unawaited(trust.load());
final rating = PeerRatingCubit(
session?.ratings,
peerPubkey: widget.peerPubkey,
selfPubkey: self,
trust: session?.trust,
);
unawaited(rating.load());
setState(() {
_messages = messages;
_trust = trust;
_rating = rating;
_peerName = cachedName;
_selfName = selfName.isEmpty ? null : selfName;
_loading = false;
@ -193,6 +192,7 @@ class _ChatScreenState extends State<ChatScreen> {
unawaited(_unread?.markRead(widget.peerPubkey));
_messages?.close();
_trust?.close();
_rating?.close();
// The session belongs to the shared connection don't close it here.
_input.dispose();
super.dispose();
@ -202,6 +202,7 @@ class _ChatScreenState extends State<ChatScreen> {
Widget build(BuildContext context) {
final messages = _messages;
final trust = _trust;
final rating = _rating;
final t = context.t;
return Scaffold(
appBar: AppBar(
@ -216,12 +217,13 @@ class _ChatScreenState extends State<ChatScreen> {
),
],
),
body: _loading || messages == null || trust == null
body: _loading || messages == null || trust == null || rating == null
? const Center(child: CircularProgressIndicator())
: MultiBlocProvider(
providers: [
BlocProvider.value(value: messages),
BlocProvider.value(value: trust),
BlocProvider.value(value: rating),
],
child: _ChatBody(
controller: _input,
@ -274,6 +276,7 @@ class _ChatBody extends StatelessWidget {
child: Column(
children: [
const _TrustBanner(),
const _RatingStrip(),
Expanded(
child: ChatMessageList(peerName: peerName, selfName: selfName),
),
@ -348,10 +351,9 @@ class _TrustBanner extends StatelessWidget {
builder: (context, state) {
final cubit = context.read<TrustCubit>();
if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
// Strongest applicable signal decides the badge (member > circle >
// vouched > unknown).
// Strongest applicable signal decides the badge (circle > vouched >
// unknown).
final (IconData icon, String label, bool strong) = switch (state.tier) {
TrustTier.networkMember => (Icons.verified, t.trust.member, true),
TrustTier.inYourCircle => (Icons.verified_user, t.trust.circle, true),
TrustTier.vouched => (
Icons.people_outline,
@ -391,6 +393,56 @@ class _TrustBanner extends StatelessWidget {
}
}
/// A slim reputation strip under the trust banner: everyone's stars for this
/// peer (highlighting how many come from people you know) and, once you've
/// actually talked with them, a button to leave or edit your own rating.
class _RatingStrip extends StatelessWidget {
const _RatingStrip();
@override
Widget build(BuildContext context) {
final t = context.t;
return BlocBuilder<PeerRatingCubit, PeerRatingState>(
builder: (context, state) {
final cubit = context.read<PeerRatingCubit>();
if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
// Soft anchor: you can only rate someone you've talked with.
final canRate = context.select<MessagesCubit, bool>(
(m) => m.state.messages.isNotEmpty,
);
if (!state.hasRatings && !canRate) return const SizedBox.shrink();
return Container(
width: double.infinity,
color: seedPrimaryContainer.withValues(alpha: 0.3),
padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 8, 4),
child: Row(
children: [
Expanded(
child: state.hasRatings
? RatingStars(
average: state.average,
count: state.count,
circleCount: state.circleCount,
)
: const SizedBox.shrink(),
),
if (canRate)
TextButton(
key: const Key('chat.rate'),
onPressed: state.busy
? null
: () => showRatingSheet(context, cubit),
child:
Text(state.iRated ? t.ratings.edit : t.ratings.rate),
),
],
),
);
},
);
}
}
/// A centered pill introducing a new day ("Today", "Yesterday", or a
/// locale-formatted date). Localized via [chatDayLabel].
class _DaySeparator extends StatelessWidget {