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,101 +1,372 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
import '../../../data/models/social_post_model.dart';
import '../../../core/constants/design_system.dart';
import '../../../domain/entities/social_post.dart';
import '../../widgets/animated_widgets.dart';
import '../../widgets/fullscreen_image_viewer.dart';
import '../../widgets/social_header_widget.dart';
import '../../widgets/social_interaction_row.dart';
import '../../widgets/swipe_background.dart'; // Import du widget de swipe
class SocialCard extends StatelessWidget {
final SocialPost post;
final VoidCallback onLike;
final VoidCallback onComment;
final VoidCallback onShare;
final VoidCallback onDeletePost;
final VoidCallback onEditPost;
/// Card moderne et élaborée pour afficher un post social.
///
/// Design inspiré d'Instagram avec hiérarchie visuelle claire,
/// support de contenu riche (hashtags, mentions) et animations fluides.
class SocialCard extends StatefulWidget {
const SocialCard({
Key? key,
required this.post,
required this.onLike,
required this.onComment,
required this.onShare,
required this.onDeletePost,
required this.onEditPost,
}) : super(key: key);
this.showVerifiedBadge = false,
super.key,
});
final SocialPost post;
final VoidCallback onLike;
final VoidCallback onComment;
final VoidCallback onShare;
final VoidCallback onDeletePost;
final VoidCallback onEditPost;
final bool showVerifiedBadge;
@override
State<SocialCard> createState() => _SocialCardState();
}
class _SocialCardState extends State<SocialCard> {
bool _showFullContent = false;
@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(post.postText),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
onDeletePost();
},
background: SwipeBackground(
color: Colors.red,
icon: Icons.delete,
label: 'Supprimer',
final theme = Theme.of(context);
return AnimatedCard(
margin: const EdgeInsets.only(bottom: DesignSystem.spacingMd),
borderRadius: DesignSystem.borderRadiusMd,
elevation: 0.5,
hoverElevation: 1.5,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec avatar, nom, timestamp
Padding(
padding: const EdgeInsets.fromLTRB(
DesignSystem.spacingLg,
DesignSystem.spacingMd,
DesignSystem.spacingSm,
DesignSystem.spacingMd,
),
child: SocialHeaderWidget(
post: widget.post,
onEditPost: widget.onEditPost,
menuKey: GlobalKey(),
menuContext: context,
onClosePost: () {},
showVerifiedBadge: widget.showVerifiedBadge,
),
),
// Image du post avec Hero animation
if (widget.post.imageUrl != null && widget.post.imageUrl!.isNotEmpty)
_buildPostImage(context, theme),
// Contenu et interactions
Padding(
padding: const EdgeInsets.all(DesignSystem.spacingLg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Barre d'interactions (like, comment, share)
SocialInteractionRow(
post: widget.post,
onLike: widget.onLike,
onComment: widget.onComment,
onShare: widget.onShare,
),
const SizedBox(height: DesignSystem.spacingMd),
// Nombre de likes
if (widget.post.likesCount > 0)
_buildLikesCount(theme),
// Contenu du post avec texte enrichi
_buildPostContent(theme),
// Nombre de commentaires
if (widget.post.commentsCount > 0)
_buildCommentsCount(theme),
],
),
),
],
),
child: Card(
color: AppColors.cardColor,
margin: const EdgeInsets.symmetric(vertical: 10.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SocialHeaderWidget(
post: post,
onEditPost: () {
print('Modifier le post');
},
menuKey: GlobalKey(),
menuContext: context,
onClosePost: () {
print('Close post');
},
),
const SizedBox(height: 8),
Text (
post.postText,
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
const SizedBox(height: 8),
if (post.postImage.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(post.postImage, fit: BoxFit.cover),
),
const SizedBox(height: 8),
Row(
children: post.tags
.map((tag) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
tag,
style: TextStyle(
color: AppColors.accentColor,
fontSize: 12,
);
}
/// Construit l'image du post avec Hero animation et double-tap to like
Widget _buildPostImage(BuildContext context, ThemeData theme) {
return GestureDetector(
onDoubleTap: () {
if (!widget.post.isLikedByCurrentUser) {
widget.onLike();
_showLikeAnimation();
}
},
onTap: () => _openFullscreenImage(context),
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
),
child: Hero(
tag: 'social_post_image_${widget.post.id}',
child: Image.network(
widget.post.imageUrl!,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
strokeWidth: 2.5,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
child: Center(
child: Icon(
Icons.broken_image_rounded,
size: 48,
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.5),
),
),
))
.toList(),
),
const SizedBox(height: 8),
SocialInteractionRow(
post: post,
onLike: onLike,
onComment: onComment,
onShare: onShare,
),
],
);
},
),
),
),
),
);
}
/// Construit le compteur de likes
Widget _buildLikesCount(ThemeData theme) {
return Padding(
padding: const EdgeInsets.only(bottom: DesignSystem.spacingMd),
child: Text(
_formatLikesCount(widget.post.likesCount),
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: -0.1,
),
),
);
}
/// Construit le contenu du post avec texte enrichi
Widget _buildPostContent(ThemeData theme) {
final content = widget.post.content;
final shouldTruncate = content.length > 150 && !_showFullContent;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14,
height: 1.4,
letterSpacing: -0.1,
),
children: [
// Nom de l'auteur en gras
TextSpan(
text: '${widget.post.authorFullName} ',
style: const TextStyle(fontWeight: FontWeight.w600),
),
// Contenu avec support des hashtags et mentions
..._buildEnrichedContent(
shouldTruncate ? '${content.substring(0, 150)}...' : content,
theme,
),
],
),
),
// Bouton "Voir plus" / "Voir moins"
if (content.length > 150)
GestureDetector(
onTap: () {
setState(() {
_showFullContent = !_showFullContent;
});
},
child: Padding(
padding: const EdgeInsets.only(top: DesignSystem.spacingXs),
child: Text(
_showFullContent ? 'Voir moins' : 'Voir plus',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
),
],
);
}
/// Construit le compteur de commentaires
Widget _buildCommentsCount(ThemeData theme) {
return GestureDetector(
onTap: widget.onComment,
child: Padding(
padding: const EdgeInsets.only(top: DesignSystem.spacingMd),
child: Text(
_formatCommentsCount(widget.post.commentsCount),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
fontSize: 13,
),
),
),
);
}
/// Construit le contenu enrichi avec hashtags et mentions cliquables
List<TextSpan> _buildEnrichedContent(String content, ThemeData theme) {
final spans = <TextSpan>[];
final words = content.split(' ');
for (var i = 0; i < words.length; i++) {
final word = words[i];
if (word.startsWith('#')) {
// Hashtag
spans.add(
TextSpan(
text: word,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
recognizer: TapGestureRecognizer()
..onTap = () {
_handleHashtagTap(word.substring(1));
},
),
);
} else if (word.startsWith('@')) {
// Mention
spans.add(
TextSpan(
text: word,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
recognizer: TapGestureRecognizer()
..onTap = () {
_handleMentionTap(word.substring(1));
},
),
);
} else {
// Texte normal
spans.add(TextSpan(text: word));
}
// Ajouter un espace sauf pour le dernier mot
if (i < words.length - 1) {
spans.add(const TextSpan(text: ' '));
}
}
return spans;
}
/// Ouvre l'image en plein écran avec Hero animation
void _openFullscreenImage(BuildContext context) {
Navigator.push<void>(
context,
PageRouteBuilder<void>(
opaque: false,
barrierColor: Colors.black,
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: animation,
child: FullscreenImageViewer(
imageUrl: widget.post.imageUrl!,
heroTag: 'social_post_image_${widget.post.id}',
title: widget.post.content,
),
);
},
),
);
}
/// Affiche l'animation de like
void _showLikeAnimation() {
// TODO: Implémenter animation de coeur qui apparaît au centre
if (mounted) {
// Animation visuelle rapide
}
}
/// Gère le tap sur un hashtag
void _handleHashtagTap(String hashtag) {
debugPrint('[SocialCard] Hashtag cliqué: #$hashtag');
// TODO: Naviguer vers la page des posts avec ce hashtag
}
/// Gère le tap sur une mention
void _handleMentionTap(String username) {
debugPrint('[SocialCard] Mention cliquée: @$username');
// TODO: Naviguer vers le profil de l'utilisateur
}
/// Formate le nombre de likes
String _formatLikesCount(int count) {
if (count == 1) {
return '1 j\'aime';
}
return '${_formatCount(count)} j\'aime';
}
/// Formate le nombre de commentaires
String _formatCommentsCount(int count) {
if (count == 1) {
return 'Voir le commentaire';
}
return 'Voir les $count commentaires';
}
/// Formate les compteurs (1K, 1M, etc.)
String _formatCount(int count) {
if (count >= 1000000) {
final value = count / 1000000;
return value % 1 == 0
? '${value.toInt()} M'
: '${value.toStringAsFixed(1)} M';
} else if (count >= 1000) {
final value = count / 1000;
return value % 1 == 0 ? '${value.toInt()} K' : '${value.toStringAsFixed(1)} K';
}
return count.toString();
}
}