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:
458
lib/presentation/widgets/shimmer_loading.dart
Normal file
458
lib/presentation/widgets/shimmer_loading.dart
Normal file
@@ -0,0 +1,458 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/constants/design_system.dart';
|
||||
|
||||
/// Widget Shimmer pour effets de chargement élégants
|
||||
///
|
||||
/// Utilisé pour afficher un état de chargement avec effet de brillance
|
||||
/// (shimmer effect) au lieu d'un simple CircularProgressIndicator.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// ShimmerLoading(
|
||||
/// child: Container(
|
||||
/// width: 200,
|
||||
/// height: 100,
|
||||
/// decoration: BoxDecoration(
|
||||
/// color: Colors.white,
|
||||
/// borderRadius: DesignSystem.borderRadiusLg,
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
class ShimmerLoading extends StatefulWidget {
|
||||
const ShimmerLoading({
|
||||
required this.child,
|
||||
this.baseColor,
|
||||
this.highlightColor,
|
||||
this.duration = const Duration(milliseconds: 1500),
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Color? baseColor;
|
||||
final Color? highlightColor;
|
||||
final Duration duration;
|
||||
|
||||
@override
|
||||
State<ShimmerLoading> createState() => _ShimmerLoadingState();
|
||||
}
|
||||
|
||||
class _ShimmerLoadingState extends State<ShimmerLoading>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
)..repeat();
|
||||
|
||||
_animation = Tween<double>(begin: -2, end: 2).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOutSine,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final baseColor = widget.baseColor ??
|
||||
(isDark ? Colors.grey[850]! : Colors.grey[300]!);
|
||||
final highlightColor = widget.highlightColor ??
|
||||
(isDark ? Colors.grey[800]! : Colors.grey[100]!);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return ShaderMask(
|
||||
shaderCallback: (bounds) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
baseColor,
|
||||
highlightColor,
|
||||
baseColor,
|
||||
],
|
||||
stops: [
|
||||
0.0,
|
||||
_animation.value,
|
||||
1.0,
|
||||
],
|
||||
transform: _SlidingGradientTransform(_animation.value),
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.srcATop,
|
||||
child: widget.child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SlidingGradientTransform extends GradientTransform {
|
||||
const _SlidingGradientTransform(this.slidePercent);
|
||||
|
||||
final double slidePercent;
|
||||
|
||||
@override
|
||||
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
|
||||
return Matrix4.translationValues(bounds.width * slidePercent, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SKELETON WIDGETS PRÉ-CONSTRUITS
|
||||
// ============================================================================
|
||||
|
||||
/// Skeleton pour une carte d'événement
|
||||
class EventCardSkeleton extends StatelessWidget {
|
||||
const EventCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
||||
|
||||
return ShimmerLoading(
|
||||
child: Card(
|
||||
margin: DesignSystem.paddingAll(DesignSystem.spacingMd),
|
||||
child: Padding(
|
||||
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image skeleton
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusLg,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
||||
// Title skeleton
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
||||
// Description skeleton
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
||||
// Buttons skeleton
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
DesignSystem.horizontalSpace(DesignSystem.spacingSm),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton pour une carte d'ami
|
||||
class FriendCardSkeleton extends StatelessWidget {
|
||||
const FriendCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
||||
|
||||
return ShimmerLoading(
|
||||
child: Container(
|
||||
margin: DesignSystem.paddingAll(DesignSystem.spacingSm),
|
||||
padding: DesignSystem.paddingAll(DesignSystem.spacingMd),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: DesignSystem.borderRadiusLg,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Avatar skeleton
|
||||
Container(
|
||||
width: DesignSystem.avatarSizeLg,
|
||||
height: DesignSystem.avatarSizeLg,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
||||
// Name skeleton
|
||||
Container(
|
||||
width: 80,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton pour un post social
|
||||
class SocialPostSkeleton extends StatelessWidget {
|
||||
const SocialPostSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
||||
|
||||
return ShimmerLoading(
|
||||
child: Card(
|
||||
margin: DesignSystem.paddingAll(DesignSystem.spacingMd),
|
||||
child: Padding(
|
||||
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header (avatar + nom)
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: DesignSystem.avatarSizeMd,
|
||||
height: DesignSystem.avatarSizeMd,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
DesignSystem.horizontalSpace(DesignSystem.spacingMd),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingXs),
|
||||
Container(
|
||||
width: 80,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
||||
// Contenu
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
||||
// Actions
|
||||
Row(
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Padding(
|
||||
padding: DesignSystem.paddingHorizontal(
|
||||
DesignSystem.spacingMd,
|
||||
),
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton générique pour liste
|
||||
class ListItemSkeleton extends StatelessWidget {
|
||||
const ListItemSkeleton({
|
||||
this.height = 80,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
||||
|
||||
return ShimmerLoading(
|
||||
child: Container(
|
||||
height: height,
|
||||
margin: DesignSystem.paddingVertical(DesignSystem.spacingSm),
|
||||
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: DesignSystem.borderRadiusLg,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
DesignSystem.horizontalSpace(DesignSystem.spacingLg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
||||
Container(
|
||||
width: 150,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: DesignSystem.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour afficher une grille de skeletons
|
||||
class SkeletonGrid extends StatelessWidget {
|
||||
const SkeletonGrid({
|
||||
required this.itemCount,
|
||||
required this.skeletonWidget,
|
||||
this.crossAxisCount = 2,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int itemCount;
|
||||
final Widget skeletonWidget;
|
||||
final int crossAxisCount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: DesignSystem.spacingMd,
|
||||
mainAxisSpacing: DesignSystem.spacingMd,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) => skeletonWidget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget pour afficher une liste de skeletons
|
||||
class SkeletonList extends StatelessWidget {
|
||||
const SkeletonList({
|
||||
required this.itemCount,
|
||||
required this.skeletonWidget,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int itemCount;
|
||||
final Widget skeletonWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) => skeletonWidget,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user