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.

This commit is contained in:
Van Leemput Dayron
2025-11-14 00:34:28 +01:00
parent c322bc079a
commit 79cf3f4655
6 changed files with 167 additions and 27 deletions

View File

@@ -260,7 +260,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: TextField( child: TextField(
@@ -268,11 +268,11 @@ class _ActivitiesPageState extends State<ActivitiesPage>
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Rechercher restaurants, musées...', hintText: 'Rechercher restaurants, musées...',
hintStyle: TextStyle( hintStyle: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6), color: theme.colorScheme.onSurface.withValues(alpha:0.6),
), ),
prefixIcon: Icon( prefixIcon: Icon(
Icons.search, Icons.search,
color: theme.colorScheme.onSurface.withOpacity(0.6), color: theme.colorScheme.onSurface.withValues(alpha:0.6),
), ),
border: InputBorder.none, border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@@ -331,7 +331,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( 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), borderRadius: BorderRadius.circular(20),
color: theme.colorScheme.surface, color: theme.colorScheme.surface,
), ),
@@ -341,13 +341,13 @@ class _ActivitiesPageState extends State<ActivitiesPage>
Icon( Icon(
icon, icon,
size: 16, size: 16,
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withValues(alpha:0.7),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
text, text,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withValues(alpha:0.7),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -362,13 +362,13 @@ class _ActivitiesPageState extends State<ActivitiesPage>
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: TabBar( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.white, labelColor: Colors.white,
unselectedLabelColor: theme.colorScheme.onSurface.withOpacity(0.7), unselectedLabelColor: theme.colorScheme.onSurface.withValues(alpha:0.7),
indicator: BoxDecoration( indicator: BoxDecoration(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@@ -814,7 +814,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
Text( Text(
'Recherche powered by Google Places', 'Recherche powered by Google Places',
style: Theme.of(context).textTheme.bodySmall?.copyWith( 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<ActivitiesPage>
Text( Text(
subtitle, subtitle,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withValues(alpha:0.7),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -977,7 +977,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1), color: theme.colorScheme.primary.withValues(alpha:0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Icon( child: Icon(
@@ -1015,7 +1015,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1), color: Colors.amber.withValues(alpha:0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
@@ -1043,7 +1043,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
Text( Text(
activity.description, activity.description,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.8), color: theme.colorScheme.onSurface.withValues(alpha:0.8),
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -1056,14 +1056,14 @@ class _ActivitiesPageState extends State<ActivitiesPage>
Icon( Icon(
Icons.location_on, Icons.location_on,
size: 16, size: 16,
color: theme.colorScheme.onSurface.withOpacity(0.6), color: theme.colorScheme.onSurface.withValues(alpha:0.6),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
activity.address!, activity.address!,
style: theme.textTheme.bodySmall?.copyWith( 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<ActivitiesPage>
vertical: 8, vertical: 8,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1), color: Colors.orange.withValues(alpha:0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: Colors.orange.withOpacity(0.3), color: Colors.orange.withValues(alpha:0.3),
), ),
), ),
child: Row( child: Row(
@@ -1140,7 +1140,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1), color: Colors.green.withValues(alpha:0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
@@ -1170,7 +1170,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1), color: Colors.red.withValues(alpha:0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(

View File

@@ -1,12 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:async';
import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/message/message_bloc.dart'; import '../../blocs/message/message_bloc.dart';
import '../../blocs/message/message_event.dart'; import '../../blocs/message/message_event.dart';
import '../../blocs/message/message_state.dart'; import '../../blocs/message/message_state.dart';
import '../../models/group.dart'; import '../../models/group.dart';
import '../../models/group_member.dart';
import '../../models/message.dart'; import '../../models/message.dart';
import '../../repositories/group_repository.dart';
/// Chat group content widget for group messaging functionality. /// Chat group content widget for group messaging functionality.
/// ///
@@ -47,17 +50,34 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
/// Currently selected message for editing (null if not editing) /// Currently selected message for editing (null if not editing)
Message? _editingMessage; Message? _editingMessage;
/// Repository pour gérer les groupes
final _groupRepository = GroupRepository();
/// Subscription pour écouter les changements des membres du groupe
late StreamSubscription<List<GroupMember>> _membersSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Load messages when the widget initializes // Load messages when the widget initializes
context.read<MessageBloc>().add(LoadMessages(widget.group.id)); context.read<MessageBloc>().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 @override
void dispose() { void dispose() {
_messageController.dispose(); _messageController.dispose();
_scrollController.dispose(); _scrollController.dispose();
_membersSubscription.cancel();
super.dispose(); super.dispose();
} }
@@ -381,12 +401,15 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
textColor = isDark ? Colors.white : Colors.black87; 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( final senderMember = widget.group.members.firstWhere(
(m) => m.userId == message.senderId, (m) => m.userId == message.senderId,
orElse: () => null as dynamic, orElse: () => null as dynamic,
) 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( return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: GestureDetector( child: GestureDetector(
@@ -413,8 +436,8 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
senderMember.profilePictureUrl == null || senderMember.profilePictureUrl == null ||
senderMember.profilePictureUrl!.isEmpty) senderMember.profilePictureUrl!.isEmpty)
? Text( ? Text(
message.senderName.isNotEmpty displayName.isNotEmpty
? message.senderName[0].toUpperCase() ? displayName[0].toUpperCase()
: '?', : '?',
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
) )
@@ -443,7 +466,7 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
children: [ children: [
if (!isMe) ...[ if (!isMe) ...[
Text( Text(
message.senderName, displayName,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -689,6 +712,9 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
? member.firstName[0].toUpperCase() ? member.firstName[0].toUpperCase()
: '?'); : '?');
// Construire le nom complet
final fullName = '${member.firstName} ${member.lastName}'.trim();
return ListTile( return ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundImage: (member.profilePictureUrl != null && backgroundImage: (member.profilePictureUrl != null &&
@@ -699,10 +725,30 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
? Text(initials) ? Text(initials)
: null, : 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' subtitle: member.role == 'admin'
? const Text('Administrateur', style: TextStyle(fontSize: 12)) ? const Text('Administrateur', style: TextStyle(fontSize: 12))
: null, : null,
trailing: IconButton(
icon: const Icon(Icons.edit, size: 18),
onPressed: () {
Navigator.pop(context);
_showChangePseudoDialog(member);
},
),
); );
}, },
), ),
@@ -716,4 +762,86 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
), ),
); );
} }
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<void> _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,
),
);
}
}
}
} }

View File

@@ -159,7 +159,7 @@ class _GroupContentState extends State<GroupContent> {
if (group.members.isNotEmpty) { if (group.members.isNotEmpty) {
final names = group.members final names = group.members
.take(2) .take(2)
.map((m) => m.pseudo.isNotEmpty ? m.pseudo : m.firstName) .map((m) => m.firstName)
.join(', '); .join(', ');
memberInfo += '\n$names'; memberInfo += '\n$names';
} }

View File

@@ -956,6 +956,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
GroupMember( GroupMember(
userId: currentUser.id, userId: currentUser.id,
firstName: currentUser.prenom, firstName: currentUser.prenom,
lastName: currentUser.nom,
pseudo: currentUser.prenom, pseudo: currentUser.prenom,
role: 'admin', role: 'admin',
profilePictureUrl: currentUser.profilePictureUrl, profilePictureUrl: currentUser.profilePictureUrl,
@@ -963,6 +964,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
...participantsData.map((p) => GroupMember( ...participantsData.map((p) => GroupMember(
userId: p['id'] as String, userId: p['id'] as String,
firstName: p['firstName'] as String, firstName: p['firstName'] as String,
lastName: p['lastName'] as String? ?? '',
pseudo: p['firstName'] as String, pseudo: p['firstName'] as String,
role: 'member', role: 'member',
profilePictureUrl: p['profilePictureUrl'] as String?, profilePictureUrl: p['profilePictureUrl'] as String?,
@@ -1151,11 +1153,13 @@ class _CreateTripContentState extends State<CreateTripContent> {
if (userId != null) { if (userId != null) {
final userDoc = await _userService.getUserById(userId); final userDoc = await _userService.getUserById(userId);
final firstName = userDoc?.prenom ?? 'Utilisateur'; final firstName = userDoc?.prenom ?? 'Utilisateur';
final lastName = userDoc?.nom ?? '';
final profilePictureUrl = userDoc?.profilePictureUrl; final profilePictureUrl = userDoc?.profilePictureUrl;
participantsData.add({ participantsData.add({
'id': userId, 'id': userId,
'firstName': firstName, 'firstName': firstName,
'lastName': lastName,
'profilePictureUrl': profilePictureUrl, 'profilePictureUrl': profilePictureUrl,
}); });
} else { } else {

View File

@@ -892,6 +892,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
final newMember = GroupMember( final newMember = GroupMember(
userId: user.id!, userId: user.id!,
firstName: user.prenom, firstName: user.prenom,
lastName: user.nom,
pseudo: user.prenom, pseudo: user.prenom,
profilePictureUrl: user.profilePictureUrl, profilePictureUrl: user.profilePictureUrl,
); );

View File

@@ -1,6 +1,7 @@
class GroupMember { class GroupMember {
final String userId; final String userId;
final String firstName; final String firstName;
final String lastName;
final String pseudo; // Pseudo du membre (par défaut = prénom) final String pseudo; // Pseudo du membre (par défaut = prénom)
final String role; // 'admin' ou 'member' final String role; // 'admin' ou 'member'
final DateTime joinedAt; final DateTime joinedAt;
@@ -9,17 +10,20 @@ class GroupMember {
GroupMember({ GroupMember({
required this.userId, required this.userId,
required this.firstName, required this.firstName,
String? lastName,
String? pseudo, String? pseudo,
this.role = 'member', this.role = 'member',
DateTime? joinedAt, DateTime? joinedAt,
this.profilePictureUrl, this.profilePictureUrl,
}) : pseudo = pseudo ?? firstName, // Par défaut, pseudo = prénom }) : lastName = lastName ?? '',
pseudo = pseudo ?? firstName,
joinedAt = joinedAt ?? DateTime.now(); joinedAt = joinedAt ?? DateTime.now();
factory GroupMember.fromMap(Map<String, dynamic> map, String userId) { factory GroupMember.fromMap(Map<String, dynamic> map, String userId) {
return GroupMember( return GroupMember(
userId: userId, userId: userId,
firstName: map['firstName'] ?? '', firstName: map['firstName'] ?? '',
lastName: map['lastName'] ?? '',
pseudo: map['pseudo'] ?? map['firstName'] ?? '', pseudo: map['pseudo'] ?? map['firstName'] ?? '',
role: map['role'] ?? 'member', role: map['role'] ?? 'member',
joinedAt: DateTime.fromMillisecondsSinceEpoch(map['joinedAt'] ?? 0), joinedAt: DateTime.fromMillisecondsSinceEpoch(map['joinedAt'] ?? 0),
@@ -30,6 +34,7 @@ class GroupMember {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'firstName': firstName, 'firstName': firstName,
'lastName': lastName,
'pseudo': pseudo, 'pseudo': pseudo,
'role': role, 'role': role,
'joinedAt': joinedAt.millisecondsSinceEpoch, 'joinedAt': joinedAt.millisecondsSinceEpoch,
@@ -40,6 +45,7 @@ class GroupMember {
GroupMember copyWith({ GroupMember copyWith({
String? userId, String? userId,
String? firstName, String? firstName,
String? lastName,
String? pseudo, String? pseudo,
String? role, String? role,
DateTime? joinedAt, DateTime? joinedAt,
@@ -48,6 +54,7 @@ class GroupMember {
return GroupMember( return GroupMember(
userId: userId ?? this.userId, userId: userId ?? this.userId,
firstName: firstName ?? this.firstName, firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
pseudo: pseudo ?? this.pseudo, pseudo: pseudo ?? this.pseudo,
role: role ?? this.role, role: role ?? this.role,
joinedAt: joinedAt ?? this.joinedAt, joinedAt: joinedAt ?? this.joinedAt,