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,25 +1,311 @@
import 'package:flutter/material.dart';
import 'social_content.dart'; // Import du fichier qui contient SocialContent
import 'package:http/http.dart' as http;
class SocialScreen extends StatelessWidget {
import '../../../core/constants/design_system.dart';
import '../../../core/constants/env_config.dart';
import '../../../core/utils/page_transitions.dart';
import '../../../data/datasources/event_remote_data_source.dart';
import '../../../data/datasources/social_remote_data_source.dart';
import '../../../data/datasources/user_remote_data_source.dart';
import '../../../data/services/secure_storage.dart';
import '../../widgets/custom_snackbar.dart';
import '../../widgets/social/create_post_dialog.dart';
import '../event/event_screen.dart';
import 'social_content.dart';
/// Écran social avec design moderne et contenu enrichi.
///
/// Cet écran affiche les posts sociaux, les stories, et les interactions
/// avec une interface utilisateur optimisée et compacte.
///
/// **Fonctionnalités:**
/// - Affichage des posts sociaux
/// - Stories
/// - Interactions (like, comment, share)
/// - Création de posts
class SocialScreen extends StatefulWidget {
const SocialScreen({super.key});
@override
State<SocialScreen> createState() => _SocialScreenState();
}
class _SocialScreenState extends State<SocialScreen> {
late final SocialRemoteDataSource _socialDataSource;
late final EventRemoteDataSource _eventDataSource;
late final SecureStorage _secureStorage;
late final UserRemoteDataSource _userDataSource;
final ValueNotifier<int> _refreshTrigger = ValueNotifier<int>(0);
String? _userId;
@override
void initState() {
super.initState();
_socialDataSource = SocialRemoteDataSource(http.Client());
_eventDataSource = EventRemoteDataSource(http.Client());
_userDataSource = UserRemoteDataSource(http.Client());
_secureStorage = SecureStorage();
_loadUserId();
}
@override
void dispose() {
_refreshTrigger.dispose();
super.dispose();
}
/// Charge l'ID utilisateur depuis le stockage sécurisé
Future<void> _loadUserId() async {
final userId = await _secureStorage.getUserId();
if (mounted) {
setState(() {
_userId = userId;
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: const Color(0xFF1E1E2C), // Fond noir pour correspondre à un thème sombre
appBar: AppBar(
title: const Text(
'Social',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.black, // AppBar avec fond noir pour un design cohérent
appBar: _buildAppBar(theme),
body: SocialContent(
userId: _userId,
refreshTrigger: _refreshTrigger,
),
body: SocialContent(), // Appel à SocialContent pour afficher le contenu
floatingActionButton: _buildFloatingActionButton(context, theme),
);
}
/// Construit la barre d'application.
PreferredSizeWidget _buildAppBar(ThemeData theme) {
return AppBar(
elevation: 0,
scrolledUnderElevation: 2,
title: Text(
'Social',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
actions: [
IconButton(
icon: const Icon(Icons.search_rounded, size: 22),
tooltip: 'Rechercher',
onPressed: _handleSearch,
),
IconButton(
icon: const Icon(Icons.add_circle_outline_rounded, size: 22),
tooltip: 'Créer un post',
onPressed: _handleCreatePost,
),
],
);
}
/// Construit le bouton flottant (compact).
Widget _buildFloatingActionButton(BuildContext context, ThemeData theme) {
return FloatingActionButton(
onPressed: _handleCreatePost,
tooltip: 'Nouveau post',
elevation: 2,
child: const Icon(Icons.add_rounded, size: 26),
);
}
/// Gère la recherche.
void _handleSearch() {
final theme = Theme.of(context);
showDialog<void>(
context: context,
builder: (context) {
final searchController = TextEditingController();
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
title: Row(
children: [
Icon(
Icons.search_rounded,
color: theme.colorScheme.primary,
size: 24,
),
const SizedBox(width: DesignSystem.spacingMd),
const Text('Rechercher'),
],
),
titleTextStyle: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 17,
),
content: TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'Posts, utilisateurs...',
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
fontSize: 14,
),
prefixIcon: Icon(
Icons.search_rounded,
size: 20,
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingMd,
vertical: DesignSystem.spacingSm,
),
),
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 14),
autofocus: true,
onSubmitted: (value) {
Navigator.pop(context);
_performSearch(value);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
_performSearch(searchController.text);
},
child: const Text('Rechercher'),
),
],
);
},
);
}
/// Effectue la recherche.
Future<void> _performSearch(String query) async {
if (query.trim().isEmpty) {
context.showWarning('Veuillez entrer un terme de recherche');
return;
}
if (EnvConfig.enableDetailedLogs) {
debugPrint('[SocialScreen] Recherche: $query');
}
try {
// Rechercher dans les événements (endpoint disponible)
final events = await _eventDataSource.searchEvents(query);
// Rechercher dans les posts sociaux (quand l'endpoint sera disponible)
// final posts = await _socialDataSource.searchPosts(query);
if (mounted) {
final totalResults = events.length; // + posts.length;
if (totalResults > 0) {
// Naviguer vers l'écran des événements avec les résultats
final userId = await _secureStorage.getUserId();
if (userId != null && mounted) {
await context.pushSlideRight<void>(
EventScreen(
userId: userId,
userFirstName: '',
userLastName: '',
profileImageUrl: '',
),
);
}
if (mounted) {
context.showSuccess(
'$totalResults résultat(s) trouvé(s) pour "$query"',
);
}
} else {
context.showInfo('Aucun résultat trouvé pour "$query"');
}
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors de la recherche: ${e.toString()}');
}
}
}
/// Gère la création d'un post avec support d'images/vidéos.
Future<void> _handleCreatePost() async {
// Récupérer les informations utilisateur pour l'affichage
final userId = await _secureStorage.getUserId();
if (userId == null || userId.isEmpty) {
if (mounted) {
context.showWarning('Vous devez être connecté pour créer un post');
}
return;
}
try {
// Récupérer les vraies informations utilisateur depuis le backend
final userModel = await _userDataSource.getUser(userId);
final userName = '${userModel.userFirstName} ${userModel.userLastName}';
final userAvatarUrl = userModel.profileImageUrl;
if (!mounted) return;
await CreatePostDialog.show(
context: context,
onPostCreated: (content, medias) => _createPost(content, medias),
userName: userName,
userAvatarUrl: userAvatarUrl,
);
} catch (e) {
if (mounted) {
context.showError(
'Erreur lors de la récupération des informations utilisateur: ${e.toString()}',
);
}
}
}
/// Crée un nouveau post avec contenu et médias.
Future<void> _createPost(String content, List<dynamic> medias) async {
try {
final userId = await _secureStorage.getUserId();
if (userId == null || userId.isEmpty) {
if (mounted) {
context.showWarning('Vous devez être connecté pour créer un post');
}
return;
}
if (EnvConfig.enableDetailedLogs) {
debugPrint('[SocialScreen] Création de post: $content');
debugPrint('[SocialScreen] Nombre de médias: ${medias.length}');
}
// TODO: Uploader les médias et récupérer les URLs
// Pour l'instant, on crée juste le post avec le contenu
await _socialDataSource.createPost(
content: content,
userId: userId,
);
if (mounted) {
context.showSuccess('Post créé avec succès');
// Actualiser automatiquement la liste des posts
_refreshTrigger.value++;
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors de la création: ${e.toString()}');
}
}
}
}