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:
dahoud
2026-01-10 10:43:17 +00:00
parent 06031b01f2
commit 92612abbd7
321 changed files with 43137 additions and 4285 deletions

View File

@@ -0,0 +1,124 @@
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant un message de chat.
///
/// Un message est échangé entre deux utilisateurs dans une conversation.
class ChatMessage extends Equatable {
const ChatMessage({
required this.id,
required this.conversationId,
required this.senderId,
required this.senderFirstName,
required this.senderLastName,
required this.senderProfileImageUrl,
required this.content,
required this.timestamp,
required this.isRead,
this.isDelivered = false,
this.attachmentUrl,
this.attachmentType,
});
final String id;
final String conversationId;
final String senderId;
final String senderFirstName;
final String senderLastName;
final String? senderProfileImageUrl;
final String content;
final DateTime timestamp;
final bool isRead;
final bool isDelivered;
final String? attachmentUrl; // URL d'une image/fichier joint
final AttachmentType? attachmentType;
/// Nom complet de l'expéditeur.
String get senderFullName => '$senderFirstName $senderLastName';
/// Indique si le message a une pièce jointe.
bool get hasAttachment => attachmentUrl != null && attachmentType != null;
@override
List<Object?> get props => [
id,
conversationId,
senderId,
senderFirstName,
senderLastName,
senderProfileImageUrl,
content,
timestamp,
isRead,
isDelivered,
attachmentUrl,
attachmentType,
];
/// Crée une copie de ce message avec des valeurs modifiées.
ChatMessage copyWith({
String? id,
String? conversationId,
String? senderId,
String? senderFirstName,
String? senderLastName,
String? senderProfileImageUrl,
String? content,
DateTime? timestamp,
bool? isRead,
bool? isDelivered,
String? attachmentUrl,
AttachmentType? attachmentType,
}) {
return ChatMessage(
id: id ?? this.id,
conversationId: conversationId ?? this.conversationId,
senderId: senderId ?? this.senderId,
senderFirstName: senderFirstName ?? this.senderFirstName,
senderLastName: senderLastName ?? this.senderLastName,
senderProfileImageUrl: senderProfileImageUrl ?? this.senderProfileImageUrl,
content: content ?? this.content,
timestamp: timestamp ?? this.timestamp,
isRead: isRead ?? this.isRead,
isDelivered: isDelivered ?? this.isDelivered,
attachmentUrl: attachmentUrl ?? this.attachmentUrl,
attachmentType: attachmentType ?? this.attachmentType,
);
}
}
/// Type de pièce jointe.
enum AttachmentType {
image,
video,
audio,
file,
}
/// Extensions pour faciliter l'utilisation.
extension AttachmentTypeExtension on AttachmentType {
String get displayName {
switch (this) {
case AttachmentType.image:
return 'Image';
case AttachmentType.video:
return 'Vidéo';
case AttachmentType.audio:
return 'Audio';
case AttachmentType.file:
return 'Fichier';
}
}
String get icon {
switch (this) {
case AttachmentType.image:
return '🖼️';
case AttachmentType.video:
return '🎥';
case AttachmentType.audio:
return '🎵';
case AttachmentType.file:
return '📄';
}
}
}

View File

@@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant un commentaire sur un post social.
///
/// Cette entité est pure et indépendante de la couche de données.
/// Elle représente un commentaire dans le domaine métier.
class Comment extends Equatable {
const Comment({
required this.id,
required this.postId,
required this.userId,
required this.userFirstName,
required this.userLastName,
required this.userProfileImageUrl,
required this.content,
required this.timestamp,
});
/// ID unique du commentaire
final String id;
/// ID du post auquel appartient ce commentaire
final String postId;
/// ID de l'utilisateur qui a créé le commentaire
final String userId;
/// Prénom de l'utilisateur
final String userFirstName;
/// Nom de l'utilisateur
final String userLastName;
/// URL de l'image de profil de l'utilisateur
final String userProfileImageUrl;
/// Contenu du commentaire
final String content;
/// Date et heure de création du commentaire
final DateTime timestamp;
/// Retourne le nom complet de l'auteur du commentaire
String get authorFullName => '$userFirstName $userLastName';
@override
List<Object?> get props => [
id,
postId,
userId,
userFirstName,
userLastName,
userProfileImageUrl,
content,
timestamp,
];
/// Crée une copie de ce commentaire avec des valeurs modifiées.
Comment copyWith({
String? id,
String? postId,
String? userId,
String? userFirstName,
String? userLastName,
String? userProfileImageUrl,
String? content,
DateTime? timestamp,
}) {
return Comment(
id: id ?? this.id,
postId: postId ?? this.postId,
userId: userId ?? this.userId,
userFirstName: userFirstName ?? this.userFirstName,
userLastName: userLastName ?? this.userLastName,
userProfileImageUrl: userProfileImageUrl ?? this.userProfileImageUrl,
content: content ?? this.content,
timestamp: timestamp ?? this.timestamp,
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:equatable/equatable.dart';
import 'chat_message.dart';
/// Entité de domaine représentant une conversation entre deux utilisateurs.
///
/// Une conversation regroupe tous les messages échangés entre deux amis.
class Conversation extends Equatable {
const Conversation({
required this.id,
required this.participantId,
required this.participantFirstName,
required this.participantLastName,
required this.participantProfileImageUrl,
this.lastMessage,
this.lastMessageTimestamp,
required this.unreadCount,
this.isTyping = false,
});
final String id;
final String participantId; // L'autre utilisateur
final String participantFirstName;
final String participantLastName;
final String? participantProfileImageUrl;
final String? lastMessage; // Contenu du dernier message
final DateTime? lastMessageTimestamp;
final int unreadCount; // Nombre de messages non lus
final bool isTyping; // L'autre utilisateur est en train de taper
/// Nom complet du participant.
String get participantFullName => '$participantFirstName $participantLastName';
/// Indique si la conversation a des messages non lus.
bool get hasUnreadMessages => unreadCount > 0;
/// Indique si la conversation a un dernier message.
bool get hasLastMessage => lastMessage != null && lastMessageTimestamp != null;
@override
List<Object?> get props => [
id,
participantId,
participantFirstName,
participantLastName,
participantProfileImageUrl,
lastMessage,
lastMessageTimestamp,
unreadCount,
isTyping,
];
/// Crée une copie de cette conversation avec des valeurs modifiées.
Conversation copyWith({
String? id,
String? participantId,
String? participantFirstName,
String? participantLastName,
String? participantProfileImageUrl,
String? lastMessage,
DateTime? lastMessageTimestamp,
int? unreadCount,
bool? isTyping,
}) {
return Conversation(
id: id ?? this.id,
participantId: participantId ?? this.participantId,
participantFirstName: participantFirstName ?? this.participantFirstName,
participantLastName: participantLastName ?? this.participantLastName,
participantProfileImageUrl: participantProfileImageUrl ?? this.participantProfileImageUrl,
lastMessage: lastMessage ?? this.lastMessage,
lastMessageTimestamp: lastMessageTimestamp ?? this.lastMessageTimestamp,
unreadCount: unreadCount ?? this.unreadCount,
isTyping: isTyping ?? this.isTyping,
);
}
}

View File

@@ -0,0 +1,216 @@
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant un établissement.
///
/// Un établissement est un lieu physique (bar, restaurant, club, etc.)
/// où peuvent se dérouler des événements Afterwork.
class Establishment extends Equatable {
const Establishment({
required this.id,
required this.name,
required this.type,
required this.address,
required this.city,
required this.postalCode,
this.description,
this.phoneNumber,
this.email,
this.website,
this.imageUrl,
this.rating,
this.priceRange,
this.capacity,
this.amenities = const [],
this.openingHours,
this.latitude,
this.longitude,
});
final String id;
final String name;
final EstablishmentType type;
final String address;
final String city;
final String postalCode;
final String? description;
final String? phoneNumber;
final String? email;
final String? website;
final String? imageUrl;
final double? rating; // Note moyenne sur 5
final PriceRange? priceRange;
final int? capacity; // Capacité maximale
final List<String> amenities; // WiFi, Terrasse, Parking, etc.
final String? openingHours;
final double? latitude;
final double? longitude;
/// Adresse complète formatée
String get fullAddress => '$address, $postalCode $city';
/// Indique si l'établissement a une localisation
bool get hasLocation => latitude != null && longitude != null;
@override
List<Object?> get props => [
id,
name,
type,
address,
city,
postalCode,
description,
phoneNumber,
email,
website,
imageUrl,
rating,
priceRange,
capacity,
amenities,
openingHours,
latitude,
longitude,
];
/// Crée une copie de cet établissement avec des valeurs modifiées.
Establishment copyWith({
String? id,
String? name,
EstablishmentType? type,
String? address,
String? city,
String? postalCode,
String? description,
String? phoneNumber,
String? email,
String? website,
String? imageUrl,
double? rating,
PriceRange? priceRange,
int? capacity,
List<String>? amenities,
String? openingHours,
double? latitude,
double? longitude,
}) {
return Establishment(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
address: address ?? this.address,
city: city ?? this.city,
postalCode: postalCode ?? this.postalCode,
description: description ?? this.description,
phoneNumber: phoneNumber ?? this.phoneNumber,
email: email ?? this.email,
website: website ?? this.website,
imageUrl: imageUrl ?? this.imageUrl,
rating: rating ?? this.rating,
priceRange: priceRange ?? this.priceRange,
capacity: capacity ?? this.capacity,
amenities: amenities ?? this.amenities,
openingHours: openingHours ?? this.openingHours,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
);
}
}
/// Type d'établissement.
enum EstablishmentType {
bar,
restaurant,
club,
cafe,
lounge,
pub,
brewery,
winery,
other,
}
/// Fourchette de prix.
enum PriceRange {
cheap, // €
moderate, // €€
expensive, // €€€
luxury, // €€€€
}
/// Extensions pour faciliter l'utilisation.
extension EstablishmentTypeExtension on EstablishmentType {
String get displayName {
switch (this) {
case EstablishmentType.bar:
return 'Bar';
case EstablishmentType.restaurant:
return 'Restaurant';
case EstablishmentType.club:
return 'Club';
case EstablishmentType.cafe:
return 'Café';
case EstablishmentType.lounge:
return 'Lounge';
case EstablishmentType.pub:
return 'Pub';
case EstablishmentType.brewery:
return 'Brasserie';
case EstablishmentType.winery:
return 'Cave à vin';
case EstablishmentType.other:
return 'Autre';
}
}
String get icon {
switch (this) {
case EstablishmentType.bar:
return '🍸';
case EstablishmentType.restaurant:
return '🍽️';
case EstablishmentType.club:
return '💃';
case EstablishmentType.cafe:
return '';
case EstablishmentType.lounge:
return '🛋️';
case EstablishmentType.pub:
return '🍺';
case EstablishmentType.brewery:
return '🍻';
case EstablishmentType.winery:
return '🍷';
case EstablishmentType.other:
return '📍';
}
}
}
extension PriceRangeExtension on PriceRange {
String get symbol {
switch (this) {
case PriceRange.cheap:
return '';
case PriceRange.moderate:
return '€€';
case PriceRange.expensive:
return '€€€';
case PriceRange.luxury:
return '€€€€';
}
}
String get displayName {
switch (this) {
case PriceRange.cheap:
return 'Économique';
case PriceRange.moderate:
return 'Modéré';
case PriceRange.expensive:
return 'Cher';
case PriceRange.luxury:
return 'Luxe';
}
}
}

View File

@@ -1,68 +1,168 @@
class EventModel {
final String eventId;
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant un événement.
///
/// Cette classe est indépendante de toute infrastructure (API, BDD, etc.)
/// et représente la logique métier pure selon Clean Architecture.
class Event extends Equatable {
const Event({
required this.id,
required this.title,
required this.description,
required this.startDate,
required this.location,
required this.category,
required this.creatorEmail, required this.creatorFirstName, required this.creatorLastName, required this.creatorProfileImageUrl, this.link,
this.imageUrl,
this.participantIds = const [],
this.status = EventStatus.open,
this.reactionsCount = 0,
this.commentsCount = 0,
this.sharesCount = 0,
});
final String id;
final String title;
final String description;
final String eventDate;
final DateTime startDate;
final String location;
final String category;
final String? link;
final String? imageUrl;
final String creatorId;
final String status;
// Informations sur le créateur
final String creatorEmail;
final String creatorFirstName;
final String creatorLastName;
final String creatorProfileImageUrl;
// Participants et interactions
final List<String> participantIds;
final EventStatus status;
final int reactionsCount;
final int commentsCount;
final int sharesCount;
EventModel({
required this.eventId,
required this.title,
required this.description,
required this.eventDate,
required this.location,
required this.category,
this.link,
this.imageUrl,
required this.creatorId,
required this.status,
});
// Méthode pour créer un EventModel à partir d'un JSON
factory EventModel.fromJson(Map<String, dynamic> json) {
return EventModel(
eventId: json['id'],
title: json['title'],
description: json['description'],
eventDate: json['event_date'],
location: json['location'],
category: json['category'],
link: json['link'],
imageUrl: json['imageUrl'],
creatorId: json['creator']['id'], // Assurez-vous que le JSON a ce format
status: json['status'],
/// Crée une copie de l'événement avec les champs modifiés
Event copyWith({
String? id,
String? title,
String? description,
DateTime? startDate,
String? location,
String? category,
String? link,
String? imageUrl,
String? creatorEmail,
String? creatorFirstName,
String? creatorLastName,
String? creatorProfileImageUrl,
List<String>? participantIds,
EventStatus? status,
int? reactionsCount,
int? commentsCount,
int? sharesCount,
}) {
return Event(
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,
creatorProfileImageUrl: creatorProfileImageUrl ?? this.creatorProfileImageUrl,
participantIds: participantIds ?? this.participantIds,
status: status ?? this.status,
reactionsCount: reactionsCount ?? this.reactionsCount,
commentsCount: commentsCount ?? this.commentsCount,
sharesCount: sharesCount ?? this.sharesCount,
);
}
// Méthode pour convertir un EventModel en JSON
Map<String, dynamic> toJson() {
return {
'id': eventId,
'title': title,
'description': description,
'event_date': eventDate,
'location': location,
'category': category,
'link': link,
'imageUrl': imageUrl,
'creator': {'id': creatorId}, // Structure du JSON pour l'API
'status': status,
};
/// Retourne le nom complet du créateur
String get creatorFullName => '$creatorFirstName $creatorLastName';
/// Retourne le nombre total de participants
int get participantsCount => participantIds.length;
/// Vérifie si l'événement est ouvert aux participations
bool get isOpen => status == EventStatus.open;
/// Vérifie si l'événement est fermé
bool get isClosed => status == EventStatus.closed;
/// Vérifie si l'événement est annulé
bool get isCancelled => status == EventStatus.cancelled;
@override
List<Object?> get props => [
id,
title,
description,
startDate,
location,
category,
link,
imageUrl,
creatorEmail,
creatorFirstName,
creatorLastName,
creatorProfileImageUrl,
participantIds,
status,
reactionsCount,
commentsCount,
sharesCount,
];
}
/// Énumération des statuts possibles d'un événement
enum EventStatus {
open,
closed,
cancelled,
completed;
/// Convertit une chaîne en EventStatus
static EventStatus fromString(String status) {
switch (status.toLowerCase()) {
case 'ouvert':
case 'open':
return EventStatus.open;
case 'fermé':
case 'ferme':
case 'closed':
return EventStatus.closed;
case 'annulé':
case 'annule':
case 'cancelled':
return EventStatus.cancelled;
case 'terminé':
case 'termine':
case 'completed':
return EventStatus.completed;
default:
return EventStatus.open;
}
}
// Convertir une liste d'EventModel à partir d'une liste JSON
static List<EventModel> fromJsonList(List<dynamic> jsonList) {
return jsonList.map((json) => EventModel.fromJson(json)).toList();
}
// Convertir une liste d'EventModel en JSON
static List<Map<String, dynamic>> toJsonList(List<EventModel> events) {
return events.map((event) => event.toJson()).toList();
/// Convertit EventStatus en chaîne pour l'API
String toApiString() {
switch (this) {
case EventStatus.open:
return 'ouvert';
case EventStatus.closed:
return 'fermé';
case EventStatus.cancelled:
return 'annulé';
case EventStatus.completed:
return 'terminé';
}
}
}

View File

@@ -11,33 +11,27 @@ enum FriendStatus { pending, accepted, blocked, unknown }
///
/// Chaque instance de [Friend] est immuable et toute modification doit passer par [copyWith].
class Friend extends Equatable {
final String friendId; // ID unique de l'ami, requis et non-nullable
final String friendFirstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données
final String friendLastName; // Nom de famille, non-nullable
final String? email; // Adresse e-mail, optionnelle mais typiquement présente
final String? imageUrl; // URL de l'image de profil, optionnelle
final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown`
final String? dateAdded;
final String? lastInteraction;
/// Logger statique pour suivre toutes les actions et transformations liées à [Friend].
static final Logger _logger = Logger();
/// Constructeur de la classe [Friend].
/// Initialisation avec des valeurs spécifiques pour `firstName` et `lastName`.
/// La validation des valeurs est incluse pour garantir l'intégrité des données.
Friend({
required this.friendId,
this.friendFirstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides
this.friendFirstName =
'Ami inconnu', // Valeur par défaut pour éviter les champs vides
this.friendLastName = '',
this.email,
this.imageUrl,
this.status = FriendStatus.unknown,
this.dateAdded,
this.lastInteraction,
this.isOnline = false,
this.isBestFriend = false,
this.hasKnownSinceChildhood = false,
}) {
assert(friendId.isNotEmpty, 'friendId ne doit pas être vide');
_logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName');
_logger.i(
'[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName',);
}
/// Méthode factory pour créer un objet [Friend] à partir d'un JSON.
@@ -49,7 +43,7 @@ class Friend extends Equatable {
if (json['friendId'] == null || (json['friendId'] as String).isEmpty) {
_logger.e('[ERROR] friendId manquant ou vide dans le JSON.');
throw ArgumentError("friendId est requis pour créer un objet Friend");
throw ArgumentError('friendId est requis pour créer un objet Friend');
}
return Friend(
@@ -59,8 +53,27 @@ class Friend extends Equatable {
email: json['email'] as String?,
imageUrl: json['friendProfileImageUrl'] as String?,
status: _parseStatus(json['status'] as String?),
isOnline: json['isOnline'] == true,
isBestFriend: json['isBestFriend'] == true,
hasKnownSinceChildhood: json['hasKnownSinceChildhood'] == true,
);
}
final String friendId; // ID unique de l'ami, requis et non-nullable
final String
friendFirstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données
final String friendLastName; // Nom de famille, non-nullable
final String? email; // Adresse e-mail, optionnelle mais typiquement présente
final String? imageUrl; // URL de l'image de profil, optionnelle
final FriendStatus
status; // Statut de l'ami, avec une valeur par défaut `unknown`
final String? dateAdded;
final String? lastInteraction;
final bool? isOnline;
final bool? isBestFriend;
final bool? hasKnownSinceChildhood;
/// Logger statique pour suivre toutes les actions et transformations liées à [Friend].
static final Logger _logger = Logger();
/// Méthode privée pour parser le champ `status` en type [FriendStatus].
/// Retourne [FriendStatus.unknown] si le statut est non reconnu.
@@ -87,6 +100,9 @@ class Friend extends Equatable {
'email': email,
'friendProfileImageUrl': imageUrl,
'status': status.name,
'isOnline': isOnline,
'isBestFriend': isBestFriend,
'hasKnownSinceChildhood': hasKnownSinceChildhood,
};
_logger.i('[LOG] Conversion Friend -> JSON : $json');
return json;
@@ -96,16 +112,18 @@ class Friend extends Equatable {
/// Facilite la modification immuable des propriétés sans affecter l'instance actuelle.
///
/// Log chaque copie pour surveiller l'état des données.
Friend copyWith({
String? friendId,
String? friendFirstName,
String? friendLastName,
String? email,
String? imageUrl,
FriendStatus? status,
String? lastInteraction,
String? dateAdded,
}) {
Friend copyWith(
{String? friendId,
String? friendFirstName,
String? friendLastName,
String? email,
String? imageUrl,
FriendStatus? status,
String? lastInteraction,
String? dateAdded,
bool? isOnline,
bool? isBestFriend,
bool? hasKnownSinceChildhood,}) {
final newFriend = Friend(
friendId: friendId ?? this.friendId,
friendFirstName: friendFirstName ?? this.friendFirstName,
@@ -113,13 +131,28 @@ class Friend extends Equatable {
email: email ?? this.email,
imageUrl: imageUrl ?? this.imageUrl,
status: status ?? this.status,
isOnline: isOnline ?? this.isOnline,
isBestFriend: isBestFriend ?? this.isBestFriend,
hasKnownSinceChildhood:
hasKnownSinceChildhood ?? this.hasKnownSinceChildhood,
);
_logger.i('[LOG] Création d\'une copie modifiée de Friend : ID = ${newFriend.friendId}');
_logger.i(
'[LOG] Création d\'une copie modifiée de Friend : ID = ${newFriend.friendId}',);
return newFriend;
}
/// Propriétés utilisées pour comparer les objets [Friend],
/// facilitant l'utilisation dans des listes et des ensembles.
@override
List<Object?> get props => [friendId, friendFirstName, friendLastName, email, imageUrl, status];
List<Object?> get props => [
friendId,
friendFirstName,
friendLastName,
email,
imageUrl,
status,
isOnline,
isBestFriend,
hasKnownSinceChildhood,
];
}

View File

@@ -0,0 +1,73 @@
import 'package:equatable/equatable.dart';
/// Entité représentant une suggestion d'ami.
///
/// Cette classe contient toutes les informations nécessaires pour
/// afficher et gérer les suggestions d'amis dans l'application.
class FriendSuggestion extends Equatable {
const FriendSuggestion({
required this.userId,
required this.firstName,
required this.lastName,
required this.email,
required this.profileImageUrl,
required this.mutualFriendsCount,
required this.suggestionReason,
});
/// ID unique de l'utilisateur suggéré
final String userId;
/// Prénom de l'utilisateur suggéré
final String firstName;
/// Nom de famille de l'utilisateur suggéré
final String lastName;
/// Adresse email de l'utilisateur suggéré
final String email;
/// URL de l'image de profil de l'utilisateur suggéré
final String profileImageUrl;
/// Nombre d'amis en commun avec cet utilisateur
final int mutualFriendsCount;
/// Raison de la suggestion (ex: "3 amis en commun")
final String suggestionReason;
/// Nom complet de l'utilisateur suggéré
String get fullName => '$firstName $lastName'.trim();
@override
List<Object?> get props => [
userId,
firstName,
lastName,
email,
profileImageUrl,
mutualFriendsCount,
suggestionReason,
];
/// Copie de l'entité avec modifications optionnelles
FriendSuggestion copyWith({
String? userId,
String? firstName,
String? lastName,
String? email,
String? profileImageUrl,
int? mutualFriendsCount,
String? suggestionReason,
}) {
return FriendSuggestion(
userId: userId ?? this.userId,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
email: email ?? this.email,
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
mutualFriendsCount: mutualFriendsCount ?? this.mutualFriendsCount,
suggestionReason: suggestionReason ?? this.suggestionReason,
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant une notification.
///
/// Cette entité est pure et indépendante de la couche de données.
/// Elle représente une notification dans le domaine métier.
class Notification extends Equatable {
const Notification({
required this.id,
required this.title,
required this.message,
required this.type,
required this.timestamp,
this.isRead = false,
this.eventId,
this.userId,
this.metadata,
});
final String id;
final String title;
final String message;
final NotificationType type;
final DateTime timestamp;
final bool isRead;
final String? eventId;
final String? userId;
final Map<String, dynamic>? metadata;
@override
List<Object?> get props => [
id,
title,
message,
type,
timestamp,
isRead,
eventId,
userId,
metadata,
];
/// Crée une copie de cette notification avec des valeurs modifiées.
Notification copyWith({
String? id,
String? title,
String? message,
NotificationType? type,
DateTime? timestamp,
bool? isRead,
String? eventId,
String? userId,
Map<String, dynamic>? metadata,
}) {
return Notification(
id: id ?? this.id,
title: title ?? this.title,
message: message ?? this.message,
type: type ?? this.type,
timestamp: timestamp ?? this.timestamp,
isRead: isRead ?? this.isRead,
eventId: eventId ?? this.eventId,
userId: userId ?? this.userId,
metadata: metadata ?? this.metadata,
);
}
}
/// Type de notification.
enum NotificationType {
event,
friend,
reminder,
other,
}

View File

@@ -0,0 +1,90 @@
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant une réservation.
///
/// Cette entité est pure et indépendante de la couche de données.
/// Elle représente une réservation d'événement ou d'établissement.
class Reservation extends Equatable {
const Reservation({
required this.id,
required this.userId,
required this.userFullName,
required this.eventId,
required this.eventTitle,
required this.reservationDate,
required this.numberOfPeople,
required this.status,
this.establishmentId,
this.establishmentName,
this.notes,
this.createdAt,
});
final String id;
final String userId;
final String userFullName;
final String eventId;
final String eventTitle;
final DateTime reservationDate;
final int numberOfPeople;
final ReservationStatus status;
final String? establishmentId;
final String? establishmentName;
final String? notes;
final DateTime? createdAt;
@override
List<Object?> get props => [
id,
userId,
userFullName,
eventId,
eventTitle,
reservationDate,
numberOfPeople,
status,
establishmentId,
establishmentName,
notes,
createdAt,
];
/// Crée une copie de cette réservation avec des valeurs modifiées.
Reservation copyWith({
String? id,
String? userId,
String? userFullName,
String? eventId,
String? eventTitle,
DateTime? reservationDate,
int? numberOfPeople,
ReservationStatus? status,
String? establishmentId,
String? establishmentName,
String? notes,
DateTime? createdAt,
}) {
return Reservation(
id: id ?? this.id,
userId: userId ?? this.userId,
userFullName: userFullName ?? this.userFullName,
eventId: eventId ?? this.eventId,
eventTitle: eventTitle ?? this.eventTitle,
reservationDate: reservationDate ?? this.reservationDate,
numberOfPeople: numberOfPeople ?? this.numberOfPeople,
status: status ?? this.status,
establishmentId: establishmentId ?? this.establishmentId,
establishmentName: establishmentName ?? this.establishmentName,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
);
}
}
/// Statut d'une réservation.
enum ReservationStatus {
pending, // En attente de confirmation
confirmed, // Confirmée
cancelled, // Annulée
completed, // Terminée
}

View File

@@ -0,0 +1,86 @@
import 'package:equatable/equatable.dart';
/// Entité de domaine représentant un post social.
///
/// Cette entité est pure et indépendante de la couche de données.
/// Elle représente un post social dans le domaine métier.
class SocialPost extends Equatable {
const SocialPost({
required this.id,
required this.content,
required this.userId,
required this.userFirstName,
required this.userLastName,
required this.userProfileImageUrl,
required this.timestamp,
this.imageUrl,
this.likesCount = 0,
this.commentsCount = 0,
this.sharesCount = 0,
this.isLikedByCurrentUser = false,
});
final String id;
final String content;
final String userId;
final String userFirstName;
final String userLastName;
final String userProfileImageUrl;
final DateTime timestamp;
final String? imageUrl;
final int likesCount;
final int commentsCount;
final int sharesCount;
final bool isLikedByCurrentUser;
/// Retourne le nom complet de l'auteur.
String get authorFullName => '$userFirstName $userLastName';
@override
List<Object?> get props => [
id,
content,
userId,
userFirstName,
userLastName,
userProfileImageUrl,
timestamp,
imageUrl,
likesCount,
commentsCount,
sharesCount,
isLikedByCurrentUser,
];
/// Crée une copie de ce post avec des valeurs modifiées.
SocialPost copyWith({
String? id,
String? content,
String? userId,
String? userFirstName,
String? userLastName,
String? userProfileImageUrl,
DateTime? timestamp,
String? imageUrl,
int? likesCount,
int? commentsCount,
int? sharesCount,
bool? isLikedByCurrentUser,
}) {
return SocialPost(
id: id ?? this.id,
content: content ?? this.content,
userId: userId ?? this.userId,
userFirstName: userFirstName ?? this.userFirstName,
userLastName: userLastName ?? this.userLastName,
userProfileImageUrl: userProfileImageUrl ?? this.userProfileImageUrl,
timestamp: timestamp ?? this.timestamp,
imageUrl: imageUrl ?? this.imageUrl,
likesCount: likesCount ?? this.likesCount,
commentsCount: commentsCount ?? this.commentsCount,
sharesCount: sharesCount ?? this.sharesCount,
isLikedByCurrentUser: isLikedByCurrentUser ?? this.isLikedByCurrentUser,
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:equatable/equatable.dart';
/// Type de média dans une story.
enum StoryMediaType {
image,
video,
}
/// Entité de domaine représentant une story.
///
/// Cette entité est pure et indépendante de la couche de données.
/// Elle représente une story éphémère (24h) dans le domaine métier.
class Story extends Equatable {
const Story({
required this.id,
required this.userId,
required this.userFirstName,
required this.userLastName,
required this.userProfileImageUrl,
required this.userIsVerified,
required this.mediaType,
required this.mediaUrl,
required this.createdAt,
required this.expiresAt,
this.thumbnailUrl,
this.durationSeconds,
this.isActive = true,
this.viewsCount = 0,
this.hasViewed = false,
});
final String id;
final String userId;
final String userFirstName;
final String userLastName;
final String userProfileImageUrl;
final bool userIsVerified;
final StoryMediaType mediaType;
final String mediaUrl;
final String? thumbnailUrl;
final int? durationSeconds;
final DateTime createdAt;
final DateTime expiresAt;
final bool isActive;
final int viewsCount;
final bool hasViewed;
/// Retourne le nom complet de l'auteur.
String get authorFullName => '$userFirstName $userLastName';
/// Vérifie si la story est expirée.
bool get isExpired => DateTime.now().isAfter(expiresAt);
/// Retourne la durée formatée (MM:SS) pour les vidéos.
String? get formattedDuration {
if (durationSeconds == null) return null;
final minutes = durationSeconds! ~/ 60;
final seconds = durationSeconds! % 60;
return '$minutes:${seconds.toString().padLeft(2, '0')}';
}
@override
List<Object?> get props => [
id,
userId,
userFirstName,
userLastName,
userProfileImageUrl,
userIsVerified,
mediaType,
mediaUrl,
thumbnailUrl,
durationSeconds,
createdAt,
expiresAt,
isActive,
viewsCount,
hasViewed,
];
Story copyWith({
String? id,
String? userId,
String? userFirstName,
String? userLastName,
String? userProfileImageUrl,
bool? userIsVerified,
StoryMediaType? mediaType,
String? mediaUrl,
String? thumbnailUrl,
int? durationSeconds,
DateTime? createdAt,
DateTime? expiresAt,
bool? isActive,
int? viewsCount,
bool? hasViewed,
}) {
return Story(
id: id ?? this.id,
userId: userId ?? this.userId,
userFirstName: userFirstName ?? this.userFirstName,
userLastName: userLastName ?? this.userLastName,
userProfileImageUrl: userProfileImageUrl ?? this.userProfileImageUrl,
userIsVerified: userIsVerified ?? this.userIsVerified,
mediaType: mediaType ?? this.mediaType,
mediaUrl: mediaUrl ?? this.mediaUrl,
thumbnailUrl: thumbnailUrl ?? this.thumbnailUrl,
durationSeconds: durationSeconds ?? this.durationSeconds,
createdAt: createdAt ?? this.createdAt,
expiresAt: expiresAt ?? this.expiresAt,
isActive: isActive ?? this.isActive,
viewsCount: viewsCount ?? this.viewsCount,
hasViewed: hasViewed ?? this.hasViewed,
);
}
}

View File

@@ -1,16 +1,6 @@
import 'package:equatable/equatable.dart';
class User extends Equatable {
final String userId;
final String userLastName;
final String userFirstName;
final String email;
final String motDePasse;
final String profileImageUrl;
final int eventsCount;
final int friendsCount;
final int postsCount;
final int visitedPlacesCount;
const User({
required this.userId,
@@ -19,11 +9,27 @@ class User extends Equatable {
required this.email,
required this.motDePasse,
required this.profileImageUrl,
this.isVerified = false,
this.eventsCount = 0,
this.friendsCount = 0,
this.postsCount = 0,
this.visitedPlacesCount = 0,
this.isOnline = false,
this.lastSeen,
});
final String userId;
final String userLastName;
final String userFirstName;
final String email;
final String motDePasse;
final String profileImageUrl;
final bool isVerified;
final int eventsCount;
final int friendsCount;
final int postsCount;
final int visitedPlacesCount;
final bool isOnline;
final DateTime? lastSeen;
@override
List<Object?> get props => [
@@ -33,9 +39,12 @@ class User extends Equatable {
email,
motDePasse,
profileImageUrl,
isVerified,
eventsCount,
friendsCount,
postsCount,
visitedPlacesCount,
isOnline,
lastSeen,
];
}

View File

@@ -0,0 +1,116 @@
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../entities/chat_message.dart';
import '../entities/conversation.dart';
/// Interface pour le repository du chat.
///
/// Cette interface définit les contrats que doit respecter tout repository
/// qui gère les données relatives aux conversations et aux messages de chat.
///
/// **Usage:**
/// ```dart
/// final result = await chatRepository.getConversations('userId');
/// result.fold(
/// (failure) => print('Erreur: $failure'),
/// (conversations) => print('${conversations.length} conversations'),
/// );
/// ```
abstract class ChatRepository {
/// Récupère toutes les conversations d'un utilisateur.
///
/// [userId] L'identifiant de l'utilisateur
///
/// Returns [Right] avec la liste des conversations si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, List<Conversation>>> getConversations(String userId);
/// Récupère ou crée une conversation avec un utilisateur.
///
/// [userId] L'identifiant de l'utilisateur actuel
/// [participantId] L'identifiant de l'autre participant
///
/// Returns [Right] avec la conversation si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, Conversation>> getOrCreateConversation(
String userId,
String participantId,
);
/// Récupère les messages d'une conversation avec pagination.
///
/// [conversationId] L'identifiant de la conversation
/// [page] Le numéro de page (défaut: 0)
/// [size] La taille de la page (défaut: 50)
///
/// Returns [Right] avec la liste des messages si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, List<ChatMessage>>> getMessages(
String conversationId, {
int page = 0,
int size = 50,
});
/// Envoie un nouveau message.
///
/// [senderId] L'identifiant de l'expéditeur
/// [recipientId] L'identifiant du destinataire
/// [content] Le contenu du message
/// [messageType] Le type de message (text, image, video, audio, file)
/// [mediaUrl] L'URL du média (optionnel)
///
/// Returns [Right] avec le message créé si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, ChatMessage>> sendMessage({
required String senderId,
required String recipientId,
required String content,
String? messageType,
String? mediaUrl,
});
/// Marque un message comme lu.
///
/// [messageId] L'identifiant du message
///
/// Returns [Right] avec void si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, void>> markMessageAsRead(String messageId);
/// Marque tous les messages d'une conversation comme lus.
///
/// [conversationId] L'identifiant de la conversation
/// [userId] L'identifiant de l'utilisateur
///
/// Returns [Right] avec void si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, void>> markConversationAsRead(
String conversationId,
String userId,
);
/// Supprime un message.
///
/// [messageId] L'identifiant du message
///
/// Returns [Right] avec void si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, void>> deleteMessage(String messageId);
/// Supprime une conversation.
///
/// [conversationId] L'identifiant de la conversation
///
/// Returns [Right] avec void si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, void>> deleteConversation(String conversationId);
/// Récupère le nombre de messages non lus pour un utilisateur.
///
/// [userId] L'identifiant de l'utilisateur
///
/// Returns [Right] avec le nombre de messages non lus si succès,
/// [Left] avec une [Failure] si erreur
Future<Either<Failure, int>> getUnreadMessagesCount(String userId);
}

View File

@@ -1,13 +1,14 @@
import 'package:afterwork/domain/entities/user.dart';
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../entities/user.dart';
/// Interface pour le dépôt de l'utilisateur.
/// Cette interface définit les contrats que doit respecter tout dépôt
/// qui gère les données relatives aux utilisateurs.
abstract class UserRepository {
/// Méthode pour récupérer un utilisateur par son identifiant.
/// Cette méthode retourne un objet [User] ou lève une exception en cas d'échec.
Future<User> getUser(String id) {
print("Appel à la méthode getUser avec l'ID : $id");
throw UnimplementedError("Cette méthode doit être implémentée dans une classe concrète.");
}
/// Cette méthode retourne [Right] avec un objet [User] si succès,
/// [Left] avec une [Failure] si erreur.
Future<Either<Failure, User>> getUser(String id);
}

View File

@@ -1,31 +1,33 @@
import 'package:dartz/dartz.dart';
import 'package:afterwork/domain/entities/user.dart';
import 'package:afterwork/domain/repositories/user_repository.dart';
import 'package:afterwork/core/errors/failures.dart';
import '../../core/errors/failures.dart';
import '../../core/utils/app_logger.dart';
import '../entities/user.dart';
import '../repositories/user_repository.dart';
/// Classe qui implémente le cas d'utilisation permettant de récupérer un utilisateur par son ID.
/// Elle interagit avec le dépôt d'utilisateur pour récupérer les données utilisateur.
class GetUser {
final UserRepository repository; // Référence au dépôt d'utilisateur
/// Constructeur qui prend en paramètre un dépôt d'utilisateur.
GetUser(this.repository) {
print("Initialisation de GetUser avec le UserRepository.");
AppLogger.d('Initialisation de GetUser avec le UserRepository.', tag: 'GetUser');
}
final UserRepository repository;
/// Méthode pour récupérer un utilisateur par son ID.
/// Retourne soit un [User], soit une [Failure] en cas d'erreur.
Future<Either<Failure, User>> call(String id) async {
print("Appel à GetUser avec l'ID : $id");
AppLogger.d("Appel à GetUser avec l'ID : $id", tag: 'GetUser');
try {
// Appel au dépôt pour récupérer l'utilisateur
final user = await repository.getUser(id);
print("Utilisateur récupéré avec succès : ${user.userId}");
return Right(user);
} catch (e) {
print("Erreur lors de la récupération de l'utilisateur : $e");
return Left(ServerFailure());
}
// Appel au dépôt pour récupérer l'utilisateur
final result = await repository.getUser(id);
// Logger le résultat
result.fold(
(failure) => AppLogger.e('Erreur lors de la récupération de l\'utilisateur', error: failure, tag: 'GetUser'),
(user) => AppLogger.d('Utilisateur récupéré avec succès : ${user.userId}', tag: 'GetUser'),
);
return result;
}
}