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,103 +1,381 @@
import 'package:flutter/material.dart';
import '../../../data/models/social_post_model.dart';
import 'social_card.dart'; // Import de la SocialCard
import 'package:http/http.dart' as http;
import '../../../core/constants/design_system.dart';
import '../../../data/datasources/social_remote_data_source.dart';
import '../../../domain/entities/social_post.dart';
import '../../widgets/comments_bottom_sheet.dart';
import '../../widgets/custom_snackbar.dart';
import '../../widgets/modern_empty_state.dart';
import '../../widgets/share_post_dialog.dart';
import '../../widgets/shimmer_loading.dart';
import 'social_card.dart';
/// Widget de contenu social moderne avec stories et feed de posts.
class SocialContent extends StatefulWidget {
const SocialContent({super.key});
const SocialContent({
super.key,
this.userId,
this.refreshTrigger,
});
/// ID de l'utilisateur pour charger les posts de ses amis
final String? userId;
/// Notifier pour déclencher un refresh depuis l'extérieur
final ValueNotifier<int>? refreshTrigger;
@override
_SocialContentState createState() => _SocialContentState();
State<SocialContent> createState() => _SocialContentState();
}
class _SocialContentState extends State<SocialContent> {
final List<SocialPost> _posts = [
SocialPost(
userName: 'John Doe',
userImage: 'lib/assets/images/profile_picture.png',
postText: 'Une belle journée au parc avec des amis ! 🌳🌞',
postImage: 'lib/assets/images/placeholder.png',
likes: 12,
comments: 4,
badges: ['Explorer', 'Photographe'],
tags: ['#Nature', '#FunDay'],
shares: 25,
),
SocialPost(
userName: 'Jane Smith',
userImage: 'lib/assets/images/profile_picture.png',
postText: 'Mon nouveau chat est tellement mignon 🐱',
postImage: 'lib/assets/images/placeholder.png',
likes: 30,
comments: 8,
badges: ['Animal Lover', 'Partageur'],
tags: ['#Chat', '#Cuteness'],
shares: 25,
),
SocialPost(
userName: 'Alice Brown',
userImage: 'lib/assets/images/profile_picture.png',
postText: 'Café du matin avec une vue magnifique ☕️',
postImage: 'lib/assets/images/placeholder.png',
likes: 45,
comments: 15,
badges: ['Gourmet', 'Partageur'],
tags: ['#Café', '#MorningVibes'],
shares: 25,
),
];
final SocialRemoteDataSource _dataSource =
SocialRemoteDataSource(http.Client());
final ScrollController _scrollController = ScrollController();
List<SocialPost> _posts = [];
bool _isLoading = true;
bool _isLoadingMore = false;
String? _errorMessage;
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _posts.length,
itemBuilder: (context, index) {
final post = _posts[index];
return SocialCard(
post: post,
onLike: () {
setState(() {
_posts[index] = SocialPost(
userName: post.userName,
userImage: post.userImage,
postText: post.postText,
postImage: post.postImage,
likes: post.likes + 1,
comments: post.comments,
badges: post.badges,
tags: post.tags,
shares: post.shares + 1,
);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Like ajouté')),
);
},
onComment: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Commentaire ajouté')),
);
},
onShare: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Post partagé')),
);
},
onDeletePost: () {
setState(() {
_posts.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Post supprimé')),
);
},
onEditPost: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Post modifié')),
);
},
);
void initState() {
super.initState();
loadPosts();
_scrollController.addListener(_onScroll);
// Écouter les changements du refreshTrigger pour actualiser
widget.refreshTrigger?.addListener(_onRefreshTriggered);
}
@override
void dispose() {
widget.refreshTrigger?.removeListener(_onRefreshTriggered);
_scrollController.dispose();
super.dispose();
}
/// Appelé quand le refreshTrigger change
void _onRefreshTriggered() {
loadPosts();
}
/// Gère le scroll pour la pagination
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 &&
!_isLoadingMore &&
!_isLoading) {
_loadMorePosts();
}
}
/// Charge les posts initiaux (méthode publique pour permettre le refresh depuis l'extérieur)
Future<void> loadPosts() async {
if (!mounted) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Si userId est fourni, charger les posts de l'utilisateur et de ses amis
// Sinon, charger tous les posts
final posts = widget.userId != null
? await _dataSource.getPostsByFriends(userId: widget.userId!)
: await _dataSource.getPosts();
if (mounted) {
setState(() {
_posts = posts.map((model) => model.toEntity()).toList();
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
}
/// Charge plus de posts (pagination)
Future<void> _loadMorePosts() async {
if (_isLoadingMore) return;
setState(() {
_isLoadingMore = true;
});
try {
// TODO: Implémenter la pagination avec offset/limit
await Future<void>.delayed(const Duration(seconds: 1));
if (mounted) {
setState(() {
_isLoadingMore = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoadingMore = false;
});
}
}
}
Future<void> _handleLike(String postId, int index) async {
final post = _posts[index];
final wasLiked = post.isLikedByCurrentUser;
// Mise à jour optimiste (UI instantanée)
setState(() {
_posts[index] = post.copyWith(
isLikedByCurrentUser: !wasLiked,
likesCount: wasLiked ? post.likesCount - 1 : post.likesCount + 1,
);
});
try {
final updatedPost = await _dataSource.likePost(postId);
if (mounted) {
setState(() {
_posts[index] = updatedPost.toEntity();
});
}
} catch (e) {
// Rollback en cas d'erreur
if (mounted) {
setState(() {
_posts[index] = post;
});
context.showError('Erreur lors du like');
}
}
}
Future<void> _handleComment(String postId, int index) async {
await CommentsBottomSheet.show(
context: context,
postId: postId,
onCommentAdded: () {
// Recharger le post pour avoir le compteur de commentaires à jour
_refreshPost(postId, index);
},
);
}
Future<void> _refreshPost(String postId, int index) async {
try {
final posts = await _dataSource.getPosts();
final updatedPost = posts.firstWhere((p) => p.id == postId);
if (mounted) {
setState(() {
_posts[index] = updatedPost.toEntity();
});
}
} catch (e) {
// Silently fail, pas besoin d'afficher une erreur
}
}
Future<void> _handleShare(String postId, int index) async {
final post = _posts[index];
// Ouvre le dialogue de partage avec plusieurs options
await SharePostDialog.show(
context: context,
post: post,
onShareConfirmed: () => _confirmShare(postId, index),
);
}
/// Confirme le partage sur le fil (incrémente le compteur)
Future<void> _confirmShare(String postId, int index) async {
try {
final updatedPost = await _dataSource.sharePost(postId);
if (mounted) {
setState(() {
_posts[index] = updatedPost.toEntity();
});
context.showSuccess('Post partagé avec succès');
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors du partage: ${e.toString()}');
}
}
}
Future<void> _handleDelete(String postId, int index) async {
try {
await _dataSource.deletePost(postId);
if (mounted) {
setState(() {
_posts.removeAt(index);
});
context.showSuccess('Post supprimé avec succès');
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors de la suppression: ${e.toString()}');
}
}
}
void _handleEdit(SocialPost post, int index) {
// TODO: Implémenter l'édition avec un dialog
context.showInfo('Fonctionnalité d\'édition à venir');
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const SkeletonList(
itemCount: 3,
skeletonWidget: SocialPostSkeleton(),
);
}
if (_errorMessage != null) {
return _buildErrorState(context);
}
if (_posts.isEmpty) {
return RefreshIndicator(
onRefresh: loadPosts,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height - 200,
child: const ModernEmptyState(
illustration: EmptyStateIllustration.social,
title: 'Aucun post disponible',
description:
'Soyez le premier à publier quelque chose et commencer les discussions !',
),
),
),
);
}
return RefreshIndicator(
onRefresh: loadPosts,
child: CustomScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// TODO: Section Stories (en attente de l'endpoint backend)
// SliverToBoxAdapter(child: _buildStoriesSection()),
// Padding top
const SliverToBoxAdapter(
child: SizedBox(height: DesignSystem.spacingSm),
),
// Liste des posts
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingMd,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= _posts.length) {
return _isLoadingMore
? const Padding(
padding: EdgeInsets.all(DesignSystem.spacingLg),
child: Center(
child: CircularProgressIndicator(strokeWidth: 2.5),
),
)
: const SizedBox.shrink();
}
final post = _posts[index];
return SocialCard(
key: ValueKey(post.id),
post: post,
onLike: () => _handleLike(post.id, index),
onComment: () => _handleComment(post.id, index),
onShare: () => _handleShare(post.id, index),
onDeletePost: () => _handleDelete(post.id, index),
onEditPost: () => _handleEdit(post, index),
// Badge vérifié retiré - pas de données backend
showVerifiedBadge: false,
);
},
childCount: _posts.length + (_isLoadingMore ? 1 : 0),
),
),
),
// Padding bottom
const SliverToBoxAdapter(
child: SizedBox(height: DesignSystem.spacingXl),
),
],
),
);
}
// TODO: Méthodes stories à implémenter quand l'endpoint backend sera disponible
// Widget _buildStoriesSection() { ... }
// Widget _buildStoryItem() { ... }
/// Construit l'état d'erreur
Widget _buildErrorState(BuildContext context) {
final theme = Theme.of(context);
return RefreshIndicator(
onRefresh: loadPosts,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height - 200,
child: Center(
child: Padding(
padding: const EdgeInsets.all(DesignSystem.spacingXl),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline_rounded,
size: 64,
color: theme.colorScheme.error.withOpacity(0.5),
),
const SizedBox(height: DesignSystem.spacingLg),
Text(
'Erreur de chargement',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: DesignSystem.spacingSm),
Text(
_errorMessage!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
const SizedBox(height: DesignSystem.spacingXl),
FilledButton.icon(
onPressed: loadPosts,
icon: const Icon(Icons.refresh_rounded, size: 20),
label: const Text('Réessayer'),
),
],
),
),
),
),
),
);
}
}