From 79cf3f4655137d52df9603d1e124bc7ed057f0e9 Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Fri, 14 Nov 2025 00:34:28 +0100 Subject: [PATCH] Enhance group member management: add last name support in GroupMember model, update member display in chat and trip details, and implement pseudo change functionality in chat group. --- .../activities/activities_page.dart | 38 ++--- lib/components/group/chat_group_content.dart | 140 +++++++++++++++++- lib/components/group/group_content.dart | 2 +- lib/components/home/create_trip_content.dart | 4 + .../home/show_trip_details_content.dart | 1 + lib/models/group_member.dart | 9 +- 6 files changed, 167 insertions(+), 27 deletions(-) diff --git a/lib/components/activities/activities_page.dart b/lib/components/activities/activities_page.dart index 3595da4..3f31a6a 100644 --- a/lib/components/activities/activities_page.dart +++ b/lib/components/activities/activities_page.dart @@ -260,7 +260,7 @@ class _ActivitiesPageState extends State padding: const EdgeInsets.all(16), child: Container( decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3), borderRadius: BorderRadius.circular(12), ), child: TextField( @@ -268,11 +268,11 @@ class _ActivitiesPageState extends State decoration: InputDecoration( hintText: 'Rechercher restaurants, musées...', hintStyle: TextStyle( - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.onSurface.withValues(alpha:0.6), ), prefixIcon: Icon( Icons.search, - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.onSurface.withValues(alpha:0.6), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( @@ -331,7 +331,7 @@ class _ActivitiesPageState extends State child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)), + border: Border.all(color: theme.colorScheme.outline.withValues(alpha:0.5)), borderRadius: BorderRadius.circular(20), color: theme.colorScheme.surface, ), @@ -341,13 +341,13 @@ class _ActivitiesPageState extends State Icon( icon, size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha:0.7), ), const SizedBox(width: 6), Text( text, style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha:0.7), fontWeight: FontWeight.w500, ), ), @@ -362,13 +362,13 @@ class _ActivitiesPageState extends State padding: const EdgeInsets.all(16), child: Container( decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3), borderRadius: BorderRadius.circular(8), ), child: TabBar( controller: _tabController, labelColor: Colors.white, - unselectedLabelColor: theme.colorScheme.onSurface.withOpacity(0.7), + unselectedLabelColor: theme.colorScheme.onSurface.withValues(alpha:0.7), indicator: BoxDecoration( color: theme.colorScheme.primary, borderRadius: BorderRadius.circular(6), @@ -814,7 +814,7 @@ class _ActivitiesPageState extends State Text( 'Recherche powered by Google Places', style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + color: Theme.of(context).colorScheme.onSurface.withValues(alpha:0.6), ), ), ], @@ -845,7 +845,7 @@ class _ActivitiesPageState extends State Text( subtitle, style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha:0.7), ), textAlign: TextAlign.center, ), @@ -977,7 +977,7 @@ class _ActivitiesPageState extends State Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: theme.colorScheme.primary.withOpacity(0.1), + color: theme.colorScheme.primary.withValues(alpha:0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -1015,7 +1015,7 @@ class _ActivitiesPageState extends State vertical: 4, ), decoration: BoxDecoration( - color: Colors.amber.withOpacity(0.1), + color: Colors.amber.withValues(alpha:0.1), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -1043,7 +1043,7 @@ class _ActivitiesPageState extends State Text( activity.description, style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.8), + color: theme.colorScheme.onSurface.withValues(alpha:0.8), ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -1056,14 +1056,14 @@ class _ActivitiesPageState extends State Icon( Icons.location_on, size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.onSurface.withValues(alpha:0.6), ), const SizedBox(width: 4), Expanded( child: Text( activity.address!, style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.onSurface.withValues(alpha:0.6), ), ), ), @@ -1084,10 +1084,10 @@ class _ActivitiesPageState extends State vertical: 8, ), decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.1), + color: Colors.orange.withValues(alpha:0.1), borderRadius: BorderRadius.circular(12), border: Border.all( - color: Colors.orange.withOpacity(0.3), + color: Colors.orange.withValues(alpha:0.3), ), ), child: Row( @@ -1140,7 +1140,7 @@ class _ActivitiesPageState extends State vertical: 4, ), decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), + color: Colors.green.withValues(alpha:0.1), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -1170,7 +1170,7 @@ class _ActivitiesPageState extends State vertical: 4, ), decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha:0.1), borderRadius: BorderRadius.circular(12), ), child: Row( diff --git a/lib/components/group/chat_group_content.dart b/lib/components/group/chat_group_content.dart index f1bce91..ede4a62 100644 --- a/lib/components/group/chat_group_content.dart +++ b/lib/components/group/chat_group_content.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:async'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/message/message_bloc.dart'; import '../../blocs/message/message_event.dart'; import '../../blocs/message/message_state.dart'; import '../../models/group.dart'; +import '../../models/group_member.dart'; import '../../models/message.dart'; +import '../../repositories/group_repository.dart'; /// Chat group content widget for group messaging functionality. /// @@ -47,17 +50,34 @@ class _ChatGroupContentState extends State { /// Currently selected message for editing (null if not editing) Message? _editingMessage; + /// Repository pour gérer les groupes + final _groupRepository = GroupRepository(); + + /// Subscription pour écouter les changements des membres du groupe + late StreamSubscription> _membersSubscription; + @override void initState() { super.initState(); // Load messages when the widget initializes context.read().add(LoadMessages(widget.group.id)); + + // Écouter les changements des membres du groupe + _membersSubscription = _groupRepository.watchGroupMembers(widget.group.id).listen((updatedMembers) { + if (mounted) { + setState(() { + widget.group.members.clear(); + widget.group.members.addAll(updatedMembers); + }); + } + }); } - + @override void dispose() { _messageController.dispose(); _scrollController.dispose(); + _membersSubscription.cancel(); super.dispose(); } @@ -381,12 +401,15 @@ class _ChatGroupContentState extends State { textColor = isDark ? Colors.white : Colors.black87; } - // Trouver le membre qui a envoyé le message pour récupérer sa photo + // Trouver le membre qui a envoyé le message pour récupérer son pseudo actuel final senderMember = widget.group.members.firstWhere( (m) => m.userId == message.senderId, orElse: () => null as dynamic, ) as dynamic; + // Utiliser le pseudo actuel du membre, ou le senderName en fallback + final displayName = senderMember != null ? senderMember.pseudo : message.senderName; + return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: GestureDetector( @@ -413,8 +436,8 @@ class _ChatGroupContentState extends State { senderMember.profilePictureUrl == null || senderMember.profilePictureUrl!.isEmpty) ? Text( - message.senderName.isNotEmpty - ? message.senderName[0].toUpperCase() + displayName.isNotEmpty + ? displayName[0].toUpperCase() : '?', style: const TextStyle(fontSize: 12), ) @@ -443,7 +466,7 @@ class _ChatGroupContentState extends State { children: [ if (!isMe) ...[ Text( - message.senderName, + displayName, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -689,6 +712,9 @@ class _ChatGroupContentState extends State { ? member.firstName[0].toUpperCase() : '?'); + // Construire le nom complet + final fullName = '${member.firstName} ${member.lastName}'.trim(); + return ListTile( leading: CircleAvatar( backgroundImage: (member.profilePictureUrl != null && @@ -699,10 +725,30 @@ class _ChatGroupContentState extends State { ? Text(initials) : null, ), - title: Text(member.pseudo), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(member.pseudo), + if (fullName.isNotEmpty) + Text( + fullName, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], + ), subtitle: member.role == 'admin' ? const Text('Administrateur', style: TextStyle(fontSize: 12)) : null, + trailing: IconButton( + icon: const Icon(Icons.edit, size: 18), + onPressed: () { + Navigator.pop(context); + _showChangePseudoDialog(member); + }, + ), ); }, ), @@ -716,4 +762,86 @@ class _ChatGroupContentState extends State { ), ); } + + void _showChangePseudoDialog(dynamic member) { + final pseudoController = TextEditingController(text: member.pseudo); + final theme = Theme.of(context); + + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: theme.dialogBackgroundColor, + title: Text( + 'Changer le pseudo', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + content: TextField( + controller: pseudoController, + decoration: InputDecoration( + hintText: 'Nouveau pseudo', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + style: TextStyle(color: theme.colorScheme.onSurface), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Annuler', + style: TextStyle(color: theme.colorScheme.primary), + ), + ), + TextButton( + onPressed: () { + final newPseudo = pseudoController.text.trim(); + if (newPseudo.isNotEmpty) { + _updateMemberPseudo(member, newPseudo); + Navigator.pop(context); + } + }, + child: Text( + 'Valider', + style: TextStyle(color: theme.colorScheme.primary), + ), + ), + ], + ), + ); + } + + Future _updateMemberPseudo(dynamic member, String newPseudo) async { + try { + final updatedMember = member.copyWith(pseudo: newPseudo); + await _groupRepository.addMember(widget.group.id, updatedMember); + + if (mounted) { + // Le stream listener va automatiquement mettre à jour les membres + // Pas besoin de fermer le dialog ou de faire un refresh manuel + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Pseudo modifié en "$newPseudo"'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la modification du pseudo: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } } \ No newline at end of file diff --git a/lib/components/group/group_content.dart b/lib/components/group/group_content.dart index 52f407c..e11d6d0 100644 --- a/lib/components/group/group_content.dart +++ b/lib/components/group/group_content.dart @@ -159,7 +159,7 @@ class _GroupContentState extends State { if (group.members.isNotEmpty) { final names = group.members .take(2) - .map((m) => m.pseudo.isNotEmpty ? m.pseudo : m.firstName) + .map((m) => m.firstName) .join(', '); memberInfo += '\n$names'; } diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 65e94ee..89b9e48 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -956,6 +956,7 @@ class _CreateTripContentState extends State { GroupMember( userId: currentUser.id, firstName: currentUser.prenom, + lastName: currentUser.nom, pseudo: currentUser.prenom, role: 'admin', profilePictureUrl: currentUser.profilePictureUrl, @@ -963,6 +964,7 @@ class _CreateTripContentState extends State { ...participantsData.map((p) => GroupMember( userId: p['id'] as String, firstName: p['firstName'] as String, + lastName: p['lastName'] as String? ?? '', pseudo: p['firstName'] as String, role: 'member', profilePictureUrl: p['profilePictureUrl'] as String?, @@ -1151,11 +1153,13 @@ class _CreateTripContentState extends State { if (userId != null) { final userDoc = await _userService.getUserById(userId); final firstName = userDoc?.prenom ?? 'Utilisateur'; + final lastName = userDoc?.nom ?? ''; final profilePictureUrl = userDoc?.profilePictureUrl; participantsData.add({ 'id': userId, 'firstName': firstName, + 'lastName': lastName, 'profilePictureUrl': profilePictureUrl, }); } else { diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index 478c6f3..6a24905 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -892,6 +892,7 @@ class _ShowTripDetailsContentState extends State { final newMember = GroupMember( userId: user.id!, firstName: user.prenom, + lastName: user.nom, pseudo: user.prenom, profilePictureUrl: user.profilePictureUrl, ); diff --git a/lib/models/group_member.dart b/lib/models/group_member.dart index 1d27f47..04f8981 100644 --- a/lib/models/group_member.dart +++ b/lib/models/group_member.dart @@ -1,6 +1,7 @@ class GroupMember { final String userId; final String firstName; + final String lastName; final String pseudo; // Pseudo du membre (par défaut = prénom) final String role; // 'admin' ou 'member' final DateTime joinedAt; @@ -9,17 +10,20 @@ class GroupMember { GroupMember({ required this.userId, required this.firstName, + String? lastName, String? pseudo, this.role = 'member', DateTime? joinedAt, this.profilePictureUrl, - }) : pseudo = pseudo ?? firstName, // Par défaut, pseudo = prénom + }) : lastName = lastName ?? '', + pseudo = pseudo ?? firstName, joinedAt = joinedAt ?? DateTime.now(); factory GroupMember.fromMap(Map map, String userId) { return GroupMember( userId: userId, firstName: map['firstName'] ?? '', + lastName: map['lastName'] ?? '', pseudo: map['pseudo'] ?? map['firstName'] ?? '', role: map['role'] ?? 'member', joinedAt: DateTime.fromMillisecondsSinceEpoch(map['joinedAt'] ?? 0), @@ -30,6 +34,7 @@ class GroupMember { Map toMap() { return { 'firstName': firstName, + 'lastName': lastName, 'pseudo': pseudo, 'role': role, 'joinedAt': joinedAt.millisecondsSinceEpoch, @@ -40,6 +45,7 @@ class GroupMember { GroupMember copyWith({ String? userId, String? firstName, + String? lastName, String? pseudo, String? role, DateTime? joinedAt, @@ -48,6 +54,7 @@ class GroupMember { return GroupMember( userId: userId ?? this.userId, firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, pseudo: pseudo ?? this.pseudo, role: role ?? this.role, joinedAt: joinedAt ?? this.joinedAt,