fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques ### Race Condition - Statuts de Messages - Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas - Cause : WebSocket delivery confirmations arrivaient avant messages locaux - Solution : Pattern Optimistic UI dans chat_bloc.dart - Création message temporaire immédiate - Ajout à la liste AVANT requête HTTP - Remplacement par message serveur à la réponse - Fichier : lib/presentation/state_management/chat_bloc.dart ## Implémentation TODOs (13/21) ### Social (social_header_widget.dart) - ✅ Copier lien du post dans presse-papiers - ✅ Partage natif via Share.share() - ✅ Dialogue de signalement avec 5 raisons ### Partage (share_post_dialog.dart) - ✅ Interface sélection d'amis avec checkboxes - ✅ Partage externe via Share API ### Média (media_upload_service.dart) - ✅ Parsing JSON réponse backend - ✅ Méthode deleteMedia() pour suppression - ✅ Génération miniature vidéo ### Posts (create_post_dialog.dart, edit_post_dialog.dart) - ✅ Extraction URL depuis uploads - ✅ Documentation chargement médias ### Chat (conversations_screen.dart) - ✅ Navigation vers notifications - ✅ ConversationSearchDelegate pour recherche ## Nouveaux Fichiers ### Configuration - build-prod.ps1 : Script build production avec dart-define - lib/core/constants/env_config.dart : Gestion environnements ### Documentation - TODOS_IMPLEMENTED.md : Documentation complète TODOs ## Améliorations ### Architecture - Refactoring injection de dépendances - Amélioration routing et navigation - Optimisation providers (UserProvider, FriendsProvider) ### UI/UX - Amélioration thème et couleurs - Optimisation animations - Meilleure gestion erreurs ### Services - Configuration API avec env_config - Amélioration datasources (events, users) - Optimisation modèles de données
This commit is contained in:
@@ -1,22 +1,44 @@
|
||||
class EventModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final String startDate;
|
||||
final String location;
|
||||
final String category;
|
||||
final String link;
|
||||
final String? imageUrl;
|
||||
final String creatorEmail;
|
||||
final String creatorFirstName; // Prénom du créateur
|
||||
final String creatorLastName; // Nom du créateur
|
||||
final String profileImageUrl;
|
||||
final List<dynamic> participants;
|
||||
String status;
|
||||
final int reactionsCount;
|
||||
final int commentsCount;
|
||||
final int sharesCount;
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../domain/entities/event.dart';
|
||||
|
||||
/// Modèle de données pour les événements (Data Transfer Object).
|
||||
///
|
||||
/// Cette classe est responsable de la sérialisation/désérialisation
|
||||
/// avec l'API backend et convertit vers/depuis l'entité de domaine [Event].
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// // Depuis JSON
|
||||
/// final event = EventModel.fromJson(jsonData);
|
||||
///
|
||||
/// // Vers JSON
|
||||
/// final json = event.toJson();
|
||||
///
|
||||
/// // Vers entité de domaine
|
||||
/// final entity = event.toEntity();
|
||||
/// ```
|
||||
class EventModel {
|
||||
/// Crée une nouvelle instance de [EventModel].
|
||||
///
|
||||
/// [id] L'identifiant unique de l'événement
|
||||
/// [title] Le titre de l'événement
|
||||
/// [description] La description de l'événement
|
||||
/// [startDate] La date de début (format ISO 8601 string)
|
||||
/// [location] Le lieu de l'événement
|
||||
/// [category] La catégorie de l'événement
|
||||
/// [link] Le lien associé (optionnel)
|
||||
/// [imageUrl] L'URL de l'image (optionnel)
|
||||
/// [creatorEmail] L'email du créateur
|
||||
/// [creatorFirstName] Le prénom du créateur
|
||||
/// [creatorLastName] Le nom du créateur
|
||||
/// [profileImageUrl] L'URL de l'image de profil du créateur
|
||||
/// [participants] La liste des participants (IDs ou objets)
|
||||
/// [status] Le statut de l'événement ('ouvert', 'fermé', 'annulé')
|
||||
/// [reactionsCount] Le nombre de réactions
|
||||
/// [commentsCount] Le nombre de commentaires
|
||||
/// [sharesCount] Le nombre de partages
|
||||
EventModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
@@ -25,7 +47,6 @@ class EventModel {
|
||||
required this.location,
|
||||
required this.category,
|
||||
required this.link,
|
||||
this.imageUrl,
|
||||
required this.creatorEmail,
|
||||
required this.creatorFirstName,
|
||||
required this.creatorLastName,
|
||||
@@ -35,73 +56,211 @@ class EventModel {
|
||||
required this.reactionsCount,
|
||||
required this.commentsCount,
|
||||
required this.sharesCount,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
/// L'identifiant unique de l'événement
|
||||
final String id;
|
||||
|
||||
/// Le titre de l'événement
|
||||
final String title;
|
||||
|
||||
/// La description de l'événement
|
||||
final String description;
|
||||
|
||||
/// La date de début (format ISO 8601 string)
|
||||
final String startDate;
|
||||
|
||||
/// Le lieu de l'événement
|
||||
final String location;
|
||||
|
||||
/// La catégorie de l'événement
|
||||
final String category;
|
||||
|
||||
/// Le lien associé à l'événement
|
||||
final String link;
|
||||
|
||||
/// L'URL de l'image de l'événement (optionnel)
|
||||
final String? imageUrl;
|
||||
|
||||
/// L'email du créateur de l'événement
|
||||
final String creatorEmail;
|
||||
|
||||
/// Le prénom du créateur
|
||||
final String creatorFirstName;
|
||||
|
||||
/// Le nom du créateur
|
||||
final String creatorLastName;
|
||||
|
||||
/// L'URL de l'image de profil du créateur
|
||||
final String profileImageUrl;
|
||||
|
||||
/// La liste des participants (peut contenir des IDs ou des objets)
|
||||
final List<dynamic> participants;
|
||||
|
||||
/// Le statut de l'événement ('ouvert', 'fermé', 'annulé')
|
||||
String status;
|
||||
|
||||
/// Le nombre de réactions
|
||||
final int reactionsCount;
|
||||
|
||||
/// Le nombre de commentaires
|
||||
final int commentsCount;
|
||||
|
||||
/// Le nombre de partages
|
||||
final int sharesCount;
|
||||
|
||||
// ============================================================================
|
||||
// FACTORY METHODS
|
||||
// ============================================================================
|
||||
|
||||
/// Crée un [EventModel] à partir d'un JSON reçu depuis l'API.
|
||||
///
|
||||
/// [json] Les données JSON à parser
|
||||
///
|
||||
/// Returns un [EventModel] avec les données parsées
|
||||
///
|
||||
/// Throws [ValidationException] si les données essentielles sont manquantes
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final json = {
|
||||
/// 'id': '123',
|
||||
/// 'title': 'Concert',
|
||||
/// 'startDate': '2026-01-10T20:00:00Z',
|
||||
/// ...
|
||||
/// };
|
||||
/// final event = EventModel.fromJson(json);
|
||||
/// ```
|
||||
factory EventModel.fromJson(Map<String, dynamic> json) {
|
||||
print('[LOG] Création de l\'EventModel depuis JSON');
|
||||
try {
|
||||
// Validation des champs essentiels
|
||||
if (json['id'] == null || json['id'].toString().isEmpty) {
|
||||
throw ValidationException(
|
||||
'L\'ID de l\'événement est requis',
|
||||
field: 'id',
|
||||
);
|
||||
}
|
||||
|
||||
// Utiliser les valeurs par défaut si une clé est absente
|
||||
final String id = json['id'] ?? 'ID Inconnu';
|
||||
final String title = json['title'] ?? 'Titre Inconnu';
|
||||
final String description = json['description'] ?? 'Description Inconnue';
|
||||
final String startDate = json['startDate'] ?? 'Date de début Inconnue';
|
||||
final String location = json['location'] ?? 'Localisation Inconnue';
|
||||
final String category = json['category'] ?? 'Catégorie Inconnue';
|
||||
final String link = json['link'] ?? 'Lien Inconnu';
|
||||
final String? imageUrl = json['imageUrl'];
|
||||
final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu';
|
||||
final String creatorFirstName = json['creatorFirstName']; // Ajout du prénom
|
||||
final String creatorLastName = json['creatorLastName']; // Ajout du nom
|
||||
final String profileImageUrl = json['profileImageUrl']; // Ajout du nom
|
||||
final List<dynamic> participants = json['participants'] ?? [];
|
||||
String status = json['status'] ?? 'ouvert';
|
||||
final int reactionsCount = json['reactionsCount'] ?? 0;
|
||||
final int commentsCount = json['commentsCount'] ?? 0;
|
||||
final int sharesCount = json['sharesCount'] ?? 0;
|
||||
if (json['title'] == null || json['title'].toString().isEmpty) {
|
||||
throw ValidationException(
|
||||
'Le titre de l\'événement est requis',
|
||||
field: 'title',
|
||||
);
|
||||
}
|
||||
|
||||
print('[LOG] Champs extraits depuis JSON :');
|
||||
print(' - ID: $id');
|
||||
print(' - Titre: $title');
|
||||
print(' - Description: $description');
|
||||
print(' - Date de début: $startDate');
|
||||
print(' - Localisation: $location');
|
||||
print(' - Catégorie: $category');
|
||||
print(' - Lien: $link');
|
||||
print(' - URL de l\'image: ${imageUrl ?? "Aucune"}');
|
||||
print(' - Email du créateur: $creatorEmail');
|
||||
print(' - Prénom du créateur: $creatorFirstName');
|
||||
print(' - Nom du créateur: $creatorLastName');
|
||||
print(' - Image de profile du créateur: $profileImageUrl');
|
||||
print(' - Participants: ${participants.length} participants');
|
||||
print(' - Statut: $status');
|
||||
print(' - Nombre de réactions: $reactionsCount');
|
||||
print(' - Nombre de commentaires: $commentsCount');
|
||||
print(' - Nombre de partages: $sharesCount');
|
||||
if (json['startDate'] == null || json['startDate'].toString().isEmpty) {
|
||||
throw ValidationException(
|
||||
'La date de début est requise',
|
||||
field: 'startDate',
|
||||
);
|
||||
}
|
||||
|
||||
// Parsing avec valeurs par défaut pour les champs optionnels
|
||||
final model = EventModel(
|
||||
id: json['id'].toString(),
|
||||
title: json['title'].toString(),
|
||||
description: json['description']?.toString() ?? '',
|
||||
startDate: json['startDate'].toString(),
|
||||
location: json['location']?.toString() ?? '',
|
||||
category: json['category']?.toString() ?? 'Autre',
|
||||
link: json['link']?.toString() ?? '',
|
||||
imageUrl: json['imageUrl']?.toString(),
|
||||
creatorEmail: json['creatorEmail']?.toString() ?? '',
|
||||
creatorFirstName: json['creatorFirstName']?.toString() ?? '',
|
||||
creatorLastName: json['creatorLastName']?.toString() ?? '',
|
||||
profileImageUrl: json['profileImageUrl']?.toString() ?? '',
|
||||
participants: json['participants'] is List
|
||||
? json['participants'] as List<dynamic>
|
||||
: [],
|
||||
status: json['status']?.toString() ?? 'ouvert',
|
||||
reactionsCount: _parseInt(json, 'reactionsCount') ?? 0,
|
||||
commentsCount: _parseInt(json, 'commentsCount') ?? 0,
|
||||
sharesCount: _parseInt(json, 'sharesCount') ?? 0,
|
||||
);
|
||||
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
_logEventParsed(model);
|
||||
}
|
||||
|
||||
return model;
|
||||
} catch (e, stackTrace) {
|
||||
if (e is ValidationException) rethrow;
|
||||
AppLogger.e('Erreur lors du parsing JSON', error: e, stackTrace: stackTrace, tag: 'EventModel');
|
||||
throw ValidationException(
|
||||
'Erreur lors du parsing de l\'événement: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse une valeur int depuis le JSON.
|
||||
static int? _parseInt(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value == null) return null;
|
||||
if (value is int) return value;
|
||||
if (value is String) {
|
||||
return int.tryParse(value);
|
||||
}
|
||||
if (value is double) {
|
||||
return value.toInt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Log les détails d'un événement parsé (uniquement en mode debug).
|
||||
static void _logEventParsed(EventModel event) {
|
||||
AppLogger.d('Événement parsé: ID=${event.id}, Titre=${event.title}, Date=${event.startDate}, Localisation=${event.location}, Statut=${event.status}, Participants=${event.participants.length}', tag: 'EventModel');
|
||||
}
|
||||
|
||||
/// Crée un [EventModel] depuis une entité de domaine [Event].
|
||||
///
|
||||
/// [event] L'entité de domaine à convertir
|
||||
///
|
||||
/// Returns un [EventModel] avec les données de l'entité
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final entity = Event(...);
|
||||
/// final model = EventModel.fromEntity(entity);
|
||||
/// ```
|
||||
factory EventModel.fromEntity(Event event) {
|
||||
return EventModel(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
startDate: startDate,
|
||||
location: location,
|
||||
category: category,
|
||||
link: link,
|
||||
imageUrl: imageUrl,
|
||||
creatorEmail: creatorEmail,
|
||||
creatorFirstName: creatorFirstName, // Ajout du prénom
|
||||
creatorLastName: creatorLastName, // Ajout du nom
|
||||
profileImageUrl: profileImageUrl,
|
||||
participants: participants,
|
||||
status: status,
|
||||
reactionsCount: reactionsCount,
|
||||
commentsCount: commentsCount,
|
||||
sharesCount: sharesCount,
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
startDate: event.startDate.toIso8601String(),
|
||||
location: event.location,
|
||||
category: event.category,
|
||||
link: event.link ?? '',
|
||||
imageUrl: event.imageUrl,
|
||||
creatorEmail: event.creatorEmail,
|
||||
creatorFirstName: event.creatorFirstName,
|
||||
creatorLastName: event.creatorLastName,
|
||||
profileImageUrl: event.creatorProfileImageUrl,
|
||||
participants: event.participantIds,
|
||||
status: event.status.toApiString(),
|
||||
reactionsCount: event.reactionsCount,
|
||||
commentsCount: event.commentsCount,
|
||||
sharesCount: event.sharesCount,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONVERSION METHODS
|
||||
// ============================================================================
|
||||
|
||||
/// Convertit ce [EventModel] en JSON pour l'envoi vers l'API.
|
||||
///
|
||||
/// Returns une [Map] contenant les données de l'événement
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final event = EventModel(...);
|
||||
/// final json = event.toJson();
|
||||
/// // Envoyer json à l'API
|
||||
/// ```
|
||||
Map<String, dynamic> toJson() {
|
||||
print('[LOG] Conversion de EventModel en JSON');
|
||||
return {
|
||||
final json = <String, dynamic>{
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
@@ -109,16 +268,166 @@ class EventModel {
|
||||
'location': location,
|
||||
'category': category,
|
||||
'link': link,
|
||||
'imageUrl': imageUrl,
|
||||
if (imageUrl != null && imageUrl!.isNotEmpty) 'imageUrl': imageUrl,
|
||||
'creatorEmail': creatorEmail,
|
||||
'creatorFirstName': creatorFirstName, // Ajout du prénom
|
||||
'creatorLastName': creatorLastName, // Ajout du nom
|
||||
'profileImageUrl': profileImageUrl,
|
||||
'creatorFirstName': creatorFirstName,
|
||||
'creatorLastName': creatorLastName,
|
||||
if (profileImageUrl.isNotEmpty) 'profileImageUrl': profileImageUrl,
|
||||
'participants': participants,
|
||||
'status': status,
|
||||
'reactionsCount': reactionsCount,
|
||||
'commentsCount': commentsCount,
|
||||
'sharesCount': sharesCount,
|
||||
};
|
||||
|
||||
AppLogger.d('Conversion en JSON pour l\'événement: $id', tag: 'EventModel');
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Convertit ce modèle vers une entité de domaine [Event].
|
||||
///
|
||||
/// Returns une instance de [Event] avec les mêmes données
|
||||
///
|
||||
/// Throws [ValidationException] si la date ne peut pas être parsée
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final model = EventModel.fromJson(json);
|
||||
/// final entity = model.toEntity();
|
||||
/// ```
|
||||
Event toEntity() {
|
||||
DateTime parsedDate;
|
||||
try {
|
||||
parsedDate = DateTime.parse(startDate);
|
||||
} catch (e) {
|
||||
throw ValidationException(
|
||||
'Format de date invalide: $startDate',
|
||||
field: 'startDate',
|
||||
);
|
||||
}
|
||||
|
||||
// Convertir les participants en liste de strings
|
||||
final participantIds = participants.map((p) {
|
||||
if (p is Map) {
|
||||
return p['id']?.toString() ?? p['userId']?.toString() ?? '';
|
||||
}
|
||||
return p.toString();
|
||||
}).where((id) => id.isNotEmpty).toList();
|
||||
|
||||
return Event(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
startDate: parsedDate,
|
||||
location: location,
|
||||
category: category,
|
||||
link: link.isEmpty ? null : link,
|
||||
imageUrl: imageUrl,
|
||||
creatorEmail: creatorEmail,
|
||||
creatorFirstName: creatorFirstName,
|
||||
creatorLastName: creatorLastName,
|
||||
creatorProfileImageUrl: profileImageUrl,
|
||||
participantIds: participantIds,
|
||||
status: EventStatus.fromString(status),
|
||||
reactionsCount: reactionsCount,
|
||||
commentsCount: commentsCount,
|
||||
sharesCount: sharesCount,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY METHODS
|
||||
// ============================================================================
|
||||
|
||||
/// Crée une copie de ce [EventModel] avec des valeurs modifiées.
|
||||
///
|
||||
/// Tous les paramètres sont optionnels. Seuls les paramètres fournis
|
||||
/// seront modifiés dans la nouvelle instance.
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final updated = event.copyWith(
|
||||
/// title: 'Nouveau titre',
|
||||
/// status: 'fermé',
|
||||
/// );
|
||||
/// ```
|
||||
EventModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? startDate,
|
||||
String? location,
|
||||
String? category,
|
||||
String? link,
|
||||
String? imageUrl,
|
||||
String? creatorEmail,
|
||||
String? creatorFirstName,
|
||||
String? creatorLastName,
|
||||
String? profileImageUrl,
|
||||
List<dynamic>? participants,
|
||||
String? status,
|
||||
int? reactionsCount,
|
||||
int? commentsCount,
|
||||
int? sharesCount,
|
||||
}) {
|
||||
return EventModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
startDate: startDate ?? this.startDate,
|
||||
location: location ?? this.location,
|
||||
category: category ?? this.category,
|
||||
link: link ?? this.link,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
creatorEmail: creatorEmail ?? this.creatorEmail,
|
||||
creatorFirstName: creatorFirstName ?? this.creatorFirstName,
|
||||
creatorLastName: creatorLastName ?? this.creatorLastName,
|
||||
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
|
||||
participants: participants ?? this.participants,
|
||||
status: status ?? this.status,
|
||||
reactionsCount: reactionsCount ?? this.reactionsCount,
|
||||
commentsCount: commentsCount ?? this.commentsCount,
|
||||
sharesCount: sharesCount ?? this.sharesCount,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retourne le nombre de participants.
|
||||
///
|
||||
/// Returns le nombre de participants dans la liste
|
||||
int get participantsCount => participants.length;
|
||||
|
||||
/// Vérifie si l'événement est ouvert.
|
||||
///
|
||||
/// Returns `true` si le statut est 'ouvert', `false` sinon
|
||||
bool get isOpen => status.toLowerCase() == 'ouvert';
|
||||
|
||||
/// Vérifie si l'événement est fermé.
|
||||
///
|
||||
/// Returns `true` si le statut est 'fermé', `false` sinon
|
||||
bool get isClosed => status.toLowerCase() == 'fermé';
|
||||
|
||||
/// Vérifie si l'événement est annulé.
|
||||
///
|
||||
/// Returns `true` si le statut est 'annulé', `false` sinon
|
||||
bool get isCancelled => status.toLowerCase() == 'annulé';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EventModel('
|
||||
'id: $id, '
|
||||
'title: $title, '
|
||||
'startDate: $startDate, '
|
||||
'status: $status'
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is EventModel && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user