## 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
449 lines
15 KiB
Dart
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),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|