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

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/design_system.dart';
import '../../../core/utils/page_transitions.dart';
import '../../../data/providers/friends_provider.dart';
import '../../../domain/entities/friend_request.dart';
import '../../widgets/add_friend_dialog.dart';
import '../../widgets/cards/friend_card.dart';
import '../../widgets/friend_request_card.dart';
import '../../widgets/search_friends.dart';
import '../../widgets/animated_widgets.dart';
import '../../widgets/custom_snackbar.dart';
import '../../widgets/friends_tab.dart';
import '../../widgets/requests_tab.dart';
/// Écran principal pour afficher et gérer la liste des amis.
///
@@ -19,6 +21,7 @@ import '../../widgets/search_friends.dart';
/// - Pagination automatique
/// - Pull-to-refresh
/// - Ajout d'amis
/// - Gestion des demandes d'amitié (reçues et envoyées)
class FriendsScreen extends StatefulWidget {
const FriendsScreen({required this.userId, super.key});
@@ -28,76 +31,91 @@ class FriendsScreen extends StatefulWidget {
State<FriendsScreen> createState() => _FriendsScreenState();
}
class _FriendsScreenState extends State<FriendsScreen> with SingleTickerProviderStateMixin {
class _FriendsScreenState extends State<FriendsScreen>
with SingleTickerProviderStateMixin {
// ============================================================================
// CONSTANTS
// ============================================================================
static const double _scrollThreshold = 200;
static const Duration _refreshDelay = Duration(milliseconds: 500);
// ============================================================================
// CONTROLLERS
// ============================================================================
late ScrollController _scrollController;
late TabController _tabController;
late final ScrollController _scrollController;
late final TabController _tabController;
// ============================================================================
// LIFECYCLE
// ============================================================================
@override
void initState() {
super.initState();
_initializeScrollController();
_tabController = TabController(length: 2, vsync: this);
_loadFriends();
_loadSentRequests();
_loadReceivedRequests();
}
void _initializeScrollController() {
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
_initializeControllers();
_loadInitialData();
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
_tabController.dispose();
_disposeControllers();
super.dispose();
}
// ============================================================================
// ACTIONS
// INITIALIZATION
// ============================================================================
/// Charge les amis au démarrage.
void _loadFriends() {
Provider.of<FriendsProvider>(context, listen: false)
.fetchFriends(widget.userId);
/// Initialise les contrôleurs.
void _initializeControllers() {
_scrollController = ScrollController()..addListener(_onScroll);
_tabController = TabController(length: 2, vsync: this);
}
/// Charge les demandes envoyées au démarrage.
void _loadSentRequests() {
Provider.of<FriendsProvider>(context, listen: false)
.fetchSentRequests();
/// Libère les ressources des contrôleurs.
void _disposeControllers() {
_scrollController
..removeListener(_onScroll)
..dispose();
_tabController.dispose();
}
/// Charge les demandes reçues au démarrage.
void _loadReceivedRequests() {
Provider.of<FriendsProvider>(context, listen: false)
.fetchReceivedRequests();
/// Charge les données initiales.
void _loadInitialData() {
final provider = Provider.of<FriendsProvider>(context, listen: false);
provider
..fetchFriends(widget.userId)
..fetchSentRequests()
..fetchReceivedRequests();
}
// ============================================================================
// SCROLL HANDLING
// ============================================================================
/// Gère le défilement pour la pagination.
void _onScroll() {
final provider = Provider.of<FriendsProvider>(context, listen: false);
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 &&
_scrollController.position.maxScrollExtent - _scrollThreshold &&
!provider.isLoading &&
provider.hasMore) {
provider.fetchFriends(widget.userId, loadMore: true);
}
}
/// Gère le rafraîchissement.
// ============================================================================
// ACTIONS
// ============================================================================
/// Gère le rafraîchissement de la liste des amis.
Future<void> _handleRefresh() async {
final provider = Provider.of<FriendsProvider>(context, listen: false);
provider.fetchFriends(widget.userId);
await Future<void>.delayed(const Duration(milliseconds: 500));
await Future<void>.delayed(_refreshDelay);
}
/// Gère l'ajout d'un ami.
@@ -105,370 +123,174 @@ class _FriendsScreenState extends State<FriendsScreen> with SingleTickerProvider
showDialog<void>(
context: context,
builder: (context) => AddFriendDialog(
onFriendAdded: () {
// Rafraîchir la liste des amis et des demandes après l'ajout
_loadFriends();
_loadSentRequests();
_loadReceivedRequests();
},
onFriendAdded: _onFriendAdded,
),
);
}
/// Callback appelé après l'ajout d'un ami.
void _onFriendAdded() {
final provider = Provider.of<FriendsProvider>(context, listen: false);
provider
..fetchFriends(widget.userId)
..fetchSentRequests()
..fetchReceivedRequests();
}
/// Gère l'actualisation manuelle.
void _handleRefreshAll() {
final provider = Provider.of<FriendsProvider>(context, listen: false);
provider
..fetchFriends(widget.userId)
..fetchSentRequests()
..fetchReceivedRequests();
}
// ============================================================================
// BUILD
// ============================================================================
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: _buildAppBar(theme),
body: _buildBody(theme),
floatingActionButton: _buildFloatingActionButton(theme),
appBar: _buildAppBar(),
body: _buildBody(),
floatingActionButton: _buildFloatingActionButton(),
);
}
/// Construit la barre d'application.
PreferredSizeWidget _buildAppBar(ThemeData theme) {
PreferredSizeWidget _buildAppBar() {
final theme = Theme.of(context);
return AppBar(
title: const Text('Mes Amis'),
elevation: 0,
scrolledUnderElevation: 2,
title: Text(
'Mes Amis',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
bottom: TabBar(
controller: _tabController,
indicatorSize: TabBarIndicatorSize.tab,
tabs: const [
Tab(text: 'Amis', icon: Icon(Icons.people)),
Tab(text: 'Demandes', icon: Icon(Icons.person_add)),
Tab(
text: 'Amis',
icon: Icon(Icons.people_rounded, size: 22),
iconMargin: EdgeInsets.only(bottom: 4),
),
Tab(
text: 'Demandes',
icon: Icon(Icons.person_add_rounded, size: 22),
iconMargin: EdgeInsets.only(bottom: 4),
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
icon: const Icon(Icons.refresh_rounded, size: 22),
tooltip: 'Actualiser',
onPressed: () {
_loadFriends();
_loadSentRequests();
_loadReceivedRequests();
},
onPressed: _handleRefreshAll,
),
],
);
}
/// Construit le corps de l'écran.
Widget _buildBody(ThemeData theme) {
Widget _buildBody() {
return SafeArea(
child: TabBarView(
controller: _tabController,
children: [
// Onglet Amis
Column(
children: [
_buildSearchBar(),
Expanded(
child: Consumer<FriendsProvider>(
builder: (context, provider, child) {
if (provider.isLoading && provider.friendsList.isEmpty) {
return _buildLoadingState(theme);
}
if (provider.friendsList.isEmpty) {
return _buildEmptyState(theme);
}
return _buildFriendsList(theme, provider);
},
),
),
],
FriendsTab(
userId: widget.userId,
scrollController: _scrollController,
onRefresh: _handleRefresh,
),
// Onglet Demandes en attente
_buildPendingRequestsTab(theme),
],
),
);
}
/// Construit la barre de recherche.
Widget _buildSearchBar() {
return const Padding(
padding: EdgeInsets.all(16),
child: SearchFriends(),
);
}
/// Construit l'état de chargement.
Widget _buildLoadingState(ThemeData theme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Chargement des amis...',
style: theme.textTheme.bodyMedium,
RequestsTab(
onAccept: _handleAcceptRequest,
onReject: _handleRejectRequest,
onCancel: _handleCancelRequest,
onRefresh: _handleRefreshAll,
),
],
),
);
}
/// Construit l'état vide.
Widget _buildEmptyState(ThemeData theme) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: theme.colorScheme.secondary.withOpacity(0.6),
),
const SizedBox(height: 24),
Text(
'Aucun ami trouvé',
style: theme.textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Commencez à ajouter des amis pour voir leurs événements',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Construit la liste des amis.
Widget _buildFriendsList(ThemeData theme, FriendsProvider provider) {
return RefreshIndicator(
onRefresh: _handleRefresh,
color: theme.colorScheme.primary,
child: GridView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemCount: provider.friendsList.length,
itemBuilder: (context, index) {
final friend = provider.friendsList[index];
return FriendCard(friend: friend);
},
),
);
}
/// Construit le bouton flottant (compact).
Widget _buildFloatingActionButton(ThemeData theme) {
/// Construit le bouton flottant.
Widget _buildFloatingActionButton() {
return FloatingActionButton(
onPressed: _handleAddFriend,
tooltip: 'Ajouter un ami',
child: const Icon(Icons.person_add),
elevation: 2,
child: const Icon(Icons.person_add_rounded, size: 24),
);
}
/// Construit l'onglet des demandes en attente avec deux sections.
Widget _buildPendingRequestsTab(ThemeData theme) {
return Consumer<FriendsProvider>(
builder: (context, provider, child) {
final isLoading = provider.isLoadingReceivedRequests || provider.isLoadingSentRequests;
final hasReceived = provider.receivedRequests.isNotEmpty;
final hasSent = provider.sentRequests.isNotEmpty;
if (isLoading && !hasReceived && !hasSent) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Chargement des demandes...',
style: theme.textTheme.bodyMedium,
),
],
),
);
}
if (!hasReceived && !hasSent) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_add_disabled,
size: 64,
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
'Aucune demande en attente',
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
const SizedBox(height: 8),
Text(
'Les demandes d\'amitié apparaîtront ici',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
await Future.wait([
provider.fetchReceivedRequests(),
provider.fetchSentRequests(),
]);
},
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
// Section Demandes reçues
if (hasReceived) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Demandes reçues',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
...provider.receivedRequests.map((request) => FriendRequestCard(
request: request,
onAccept: () => _handleAcceptRequest(provider, request.friendshipId),
onReject: () => _handleRejectRequest(provider, request.friendshipId),
)),
const SizedBox(height: 16),
],
// Section Demandes envoyées
if (hasSent) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Demandes envoyées',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
...provider.sentRequests.map((request) => FriendRequestCard(
request: request,
onAccept: null, // Pas d'accepter pour les demandes envoyées
onReject: () => _handleCancelRequest(provider, request.friendshipId),
isSentRequest: true, // Indique que c'est une demande envoyée
)),
],
],
),
);
},
);
}
// ============================================================================
// REQUEST HANDLERS
// ============================================================================
/// Gère l'acceptation d'une demande d'amitié.
Future<void> _handleAcceptRequest(FriendsProvider provider, String friendshipId) async {
Future<void> _handleAcceptRequest(
FriendsProvider provider,
String friendshipId,
) async {
try {
await provider.acceptFriendRequest(friendshipId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Demande d\'amitié acceptée'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
// Rafraîchir les deux onglets
_loadFriends();
_loadReceivedRequests();
}
if (!mounted) return;
context.showSuccess('Demande d\'amitié acceptée');
_refreshAfterRequest();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
if (!mounted) return;
context.showError('Erreur: ${e.toString()}');
}
}
/// Gère le rejet d'une demande d'amitié.
Future<void> _handleRejectRequest(FriendsProvider provider, String friendshipId) async {
Future<void> _handleRejectRequest(
FriendsProvider provider,
String friendshipId,
) async {
try {
await provider.rejectFriendRequest(friendshipId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Demande d\'amitié rejetée'),
backgroundColor: Colors.orange,
behavior: SnackBarBehavior.floating,
),
);
_loadReceivedRequests();
}
if (!mounted) return;
context.showWarning('Demande d\'amitié rejetée');
provider.fetchReceivedRequests();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
if (!mounted) return;
context.showError('Erreur: ${e.toString()}');
}
}
/// Gère l'annulation d'une demande d'amitié envoyée.
Future<void> _handleCancelRequest(FriendsProvider provider, String friendshipId) async {
Future<void> _handleCancelRequest(
FriendsProvider provider,
String friendshipId,
) async {
try {
await provider.cancelFriendRequest(friendshipId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Demande d\'amitié annulée'),
backgroundColor: Colors.blue,
behavior: SnackBarBehavior.floating,
),
);
_loadSentRequests();
}
if (!mounted) return;
context.showInfo('Demande d\'amitié annulée');
provider.fetchSentRequests();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
if (!mounted) return;
context.showError('Erreur: ${e.toString()}');
}
}
// ============================================================================
// HELPERS
// ============================================================================
/// Rafraîchit les données après une action sur une demande.
void _refreshAfterRequest() {
final provider = Provider.of<FriendsProvider>(context, listen: false);
provider
..fetchFriends(widget.userId)
..fetchReceivedRequests();
}
}