Files
afterwork/lib/presentation/widgets/comments_bottom_sheet.dart
dahoud 92612abbd7 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
2026-01-10 10:43:17 +00:00

449 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../../core/constants/design_system.dart';
import '../../core/utils/calculate_time_ago.dart';
import '../../data/datasources/social_remote_data_source.dart';
import '../../data/services/secure_storage.dart';
import '../../domain/entities/comment.dart';
import 'animated_widgets.dart';
import 'custom_snackbar.dart';
import 'shimmer_loading.dart';
/// Bottom sheet moderne pour afficher et ajouter des commentaires.
///
/// Ce widget affiche tous les commentaires d'un post et permet
/// d'ajouter de nouveaux commentaires avec une interface élégante.
///
/// **Usage:**
/// ```dart
/// showCommentsBottomSheet(
/// context: context,
/// postId: '123',
/// );
/// ```
class CommentsBottomSheet extends StatefulWidget {
const CommentsBottomSheet({
required this.postId,
required this.onCommentAdded,
super.key,
});
/// ID du post
final String postId;
/// Callback appelé quand un commentaire est ajouté
final VoidCallback onCommentAdded;
/// Méthode statique pour afficher le bottom sheet
static Future<void> show({
required BuildContext context,
required String postId,
required VoidCallback onCommentAdded,
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => CommentsBottomSheet(
postId: postId,
onCommentAdded: onCommentAdded,
),
);
}
@override
State<CommentsBottomSheet> createState() => _CommentsBottomSheetState();
}
class _CommentsBottomSheetState extends State<CommentsBottomSheet> {
final SocialRemoteDataSource _dataSource = SocialRemoteDataSource(http.Client());
final SecureStorage _secureStorage = SecureStorage();
final TextEditingController _commentController = TextEditingController();
final FocusNode _commentFocusNode = FocusNode();
List<Comment> _comments = [];
bool _isLoading = true;
bool _isSubmitting = false;
String? _currentUserId;
@override
void initState() {
super.initState();
_loadComments();
_loadCurrentUser();
}
@override
void dispose() {
_commentController.dispose();
_commentFocusNode.dispose();
super.dispose();
}
Future<void> _loadCurrentUser() async {
final userId = await _secureStorage.getUserId();
if (mounted) {
setState(() {
_currentUserId = userId;
});
}
}
Future<void> _loadComments() async {
setState(() {
_isLoading = true;
});
try {
final comments = await _dataSource.getComments(widget.postId);
if (mounted) {
setState(() {
_comments = comments.map((model) => model.toEntity()).toList();
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
context.showError('Erreur lors du chargement des commentaires');
}
}
}
Future<void> _submitComment() async {
final content = _commentController.text.trim();
if (content.isEmpty) {
context.showWarning('Le commentaire ne peut pas être vide');
return;
}
if (_currentUserId == null) {
context.showWarning('Vous devez être connecté pour commenter');
return;
}
setState(() {
_isSubmitting = true;
});
try {
final newComment = await _dataSource.createComment(
postId: widget.postId,
content: content,
userId: _currentUserId!,
);
if (mounted) {
setState(() {
_comments.insert(0, newComment.toEntity());
_isSubmitting = false;
_commentController.clear();
});
_commentFocusNode.unfocus();
widget.onCommentAdded();
context.showSuccess('Commentaire ajouté');
}
} catch (e) {
if (mounted) {
setState(() {
_isSubmitting = false;
});
context.showError('Erreur lors de l\'ajout du commentaire');
}
}
}
Future<void> _deleteComment(Comment comment, int index) async {
try {
await _dataSource.deleteComment(widget.postId, comment.id);
if (mounted) {
setState(() {
_comments.removeAt(index);
});
widget.onCommentAdded();
context.showSuccess('Commentaire supprimé');
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors de la suppression');
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
return Container(
height: mediaQuery.size.height * 0.75,
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(DesignSystem.radiusLg),
),
),
child: Column(
children: [
// Handle bar
Container(
margin: const EdgeInsets.symmetric(vertical: DesignSystem.spacingSm),
width: 36,
height: 3,
decoration: BoxDecoration(
color: theme.colorScheme.onSurface.withOpacity(0.2),
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
),
),
// Header
Padding(
padding: DesignSystem.paddingHorizontal(DesignSystem.spacingLg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Commentaires',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
Text(
'${_comments.length}',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
fontSize: 14,
),
),
],
),
),
Divider(
height: 20,
thickness: 1,
color: theme.dividerColor.withOpacity(0.5),
),
// Liste des commentaires
Expanded(
child: _isLoading
? const SkeletonList(
itemCount: 3,
skeletonWidget: ListItemSkeleton(),
)
: _comments.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat_bubble_outline_rounded,
size: 56,
color: theme.colorScheme.onSurface.withOpacity(0.2),
),
const SizedBox(height: DesignSystem.spacingMd),
Text(
'Aucun commentaire',
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: DesignSystem.spacingSm),
Text(
'Soyez le premier à commenter',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
fontSize: 13,
),
),
],
),
)
: ListView.separated(
padding: DesignSystem.paddingAll(DesignSystem.spacingMd),
itemCount: _comments.length,
separatorBuilder: (context, index) => Divider(
height: 20,
thickness: 1,
color: theme.dividerColor.withOpacity(0.3),
),
itemBuilder: (context, index) {
final comment = _comments[index];
return _buildCommentItem(comment, index, theme);
},
),
),
// Input pour ajouter un commentaire
Container(
padding: EdgeInsets.only(
left: DesignSystem.spacingLg,
right: DesignSystem.spacingLg,
top: DesignSystem.spacingMd,
bottom: mediaQuery.viewInsets.bottom + DesignSystem.spacingMd,
),
decoration: BoxDecoration(
color: theme.cardColor,
border: Border(
top: BorderSide(
color: theme.dividerColor.withOpacity(0.3),
width: 1,
),
),
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: TextField(
controller: _commentController,
focusNode: _commentFocusNode,
decoration: InputDecoration(
hintText: 'Ajouter un commentaire...',
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
fontSize: 14,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
borderSide: BorderSide.none,
),
filled: true,
fillColor: theme.colorScheme.surfaceVariant.withOpacity(0.5),
contentPadding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingMd,
vertical: DesignSystem.spacingSm,
),
),
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 14),
maxLines: null,
textCapitalization: TextCapitalization.sentences,
enabled: !_isSubmitting,
),
),
const SizedBox(width: DesignSystem.spacingSm),
AnimatedScaleButton(
onTap: _isSubmitting ? () {} : _submitComment,
child: Container(
padding: const EdgeInsets.all(DesignSystem.spacingSm),
decoration: BoxDecoration(
color: _isSubmitting
? theme.colorScheme.primary.withOpacity(0.5)
: theme.colorScheme.primary,
shape: BoxShape.circle,
),
child: _isSubmitting
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: theme.colorScheme.onPrimary,
),
)
: Icon(
Icons.send_rounded,
color: theme.colorScheme.onPrimary,
size: 18,
),
),
),
],
),
),
),
],
),
);
}
Widget _buildCommentItem(Comment comment, int index, ThemeData theme) {
final isCurrentUser = comment.userId == _currentUserId;
return FadeInWidget(
delay: Duration(milliseconds: index * 50),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar
CircleAvatar(
radius: 18,
backgroundImage: comment.userProfileImageUrl.isNotEmpty
? NetworkImage(comment.userProfileImageUrl)
: null,
backgroundColor: theme.colorScheme.primary.withOpacity(0.15),
child: comment.userProfileImageUrl.isEmpty
? Icon(
Icons.person_rounded,
color: theme.colorScheme.primary,
size: 18,
)
: null,
),
const SizedBox(width: DesignSystem.spacingMd),
// Contenu du commentaire
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Nom et timestamp
Row(
children: [
Expanded(
child: Text(
comment.authorFullName,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
Text(
calculateTimeAgo(comment.timestamp),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
fontSize: 12,
),
),
],
),
const SizedBox(height: 4),
// Contenu
Text(
comment.content,
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14,
height: 1.4,
),
),
],
),
),
// Bouton supprimer (si c'est le commentaire de l'utilisateur)
if (isCurrentUser) ...[
const SizedBox(width: DesignSystem.spacingSm),
IconButton(
icon: const Icon(Icons.delete_outline_rounded, size: 18),
color: theme.colorScheme.error.withOpacity(0.7),
onPressed: () => _deleteComment(comment, index),
tooltip: 'Supprimer',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
],
],
),
);
}
}