Files
afterwork/lib/presentation/widgets/in_app_notification.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

257 lines
8.3 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/constants/design_system.dart';
import '../../domain/entities/notification.dart' as domain;
import 'animated_widgets.dart';
/// Notification in-app affichée en overlay au-dessus du contenu.
///
/// S'affiche en haut de l'écran avec une animation de slide down
/// et disparaît automatiquement après quelques secondes.
///
/// **Usage:**
/// ```dart
/// InAppNotification.show(
/// context: context,
/// notification: myNotification,
/// onTap: () {
/// // Naviguer vers la notification
/// },
/// );
/// ```
class InAppNotification {
/// Affiche une notification in-app en overlay.
static void show({
required BuildContext context,
required domain.Notification notification,
Duration duration = const Duration(seconds: 4),
VoidCallback? onTap,
VoidCallback? onDismiss,
}) {
final overlay = Overlay.of(context);
late OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
builder: (context) => _InAppNotificationWidget(
notification: notification,
onTap: () {
overlayEntry.remove();
onTap?.call();
},
onDismiss: () {
overlayEntry.remove();
onDismiss?.call();
},
),
);
overlay.insert(overlayEntry);
// Supprime automatiquement après la durée spécifiée
Future.delayed(duration, () {
if (overlayEntry.mounted) {
overlayEntry.remove();
onDismiss?.call();
}
});
}
}
class _InAppNotificationWidget extends StatefulWidget {
const _InAppNotificationWidget({
required this.notification,
required this.onTap,
required this.onDismiss,
});
final domain.Notification notification;
final VoidCallback onTap;
final VoidCallback onDismiss;
@override
State<_InAppNotificationWidget> createState() =>
_InAppNotificationWidgetState();
}
class _InAppNotificationWidgetState extends State<_InAppNotificationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.durationNormal,
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeIn,
),
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Positioned(
top: 0,
left: 0,
right: 0,
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(DesignSystem.paddingMedium),
child: Dismissible(
key: ValueKey(widget.notification.id),
direction: DismissDirection.up,
onDismissed: (direction) {
widget.onDismiss();
},
child: AnimatedScaleButton(
onTap: widget.onTap,
child: Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(DesignSystem.radiusMedium),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(DesignSystem.radiusMedium),
child: Padding(
padding: const EdgeInsets.all(DesignSystem.paddingMedium),
child: Row(
children: [
// Icône selon le type
Container(
padding: const EdgeInsets.all(DesignSystem.paddingSmall),
decoration: BoxDecoration(
color: _getNotificationColor(widget.notification.type)
.withOpacity(0.2),
borderRadius: BorderRadius.circular(DesignSystem.radiusSmall),
),
child: Icon(
_getNotificationIcon(widget.notification.type),
color: _getNotificationColor(widget.notification.type),
size: 24,
),
),
const SizedBox(width: DesignSystem.paddingMedium),
// Contenu
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.notification.title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
widget.notification.message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
// Bouton fermer
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: widget.onDismiss,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
],
),
),
),
),
),
),
),
),
),
),
),
);
}
Color _getNotificationColor(domain.NotificationType type) {
switch (type) {
case domain.NotificationType.event:
return Colors.blue;
case domain.NotificationType.friend:
return Colors.green;
case domain.NotificationType.reminder:
return Colors.orange;
case domain.NotificationType.other:
return Colors.grey;
}
}
IconData _getNotificationIcon(domain.NotificationType type) {
switch (type) {
case domain.NotificationType.event:
return Icons.event;
case domain.NotificationType.friend:
return Icons.person_add;
case domain.NotificationType.reminder:
return Icons.alarm;
case domain.NotificationType.other:
return Icons.notifications;
}
}
}