import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import '../models/chat_model.dart'; class MessageBubble extends StatefulWidget { final String body; final String? rawBody; final String? statusLabel; final int date; final bool isMe; final MessageStatus status; final bool isSecure; final bool canRetryDecryption; final VoidCallback? onRetryDecryption; final String? packetMode; final bool isPendingMultipart; const MessageBubble({ super.key, required this.body, this.rawBody, this.statusLabel, required this.date, required this.isMe, required this.status, this.isSecure = false, this.canRetryDecryption = false, this.onRetryDecryption, this.packetMode, this.isPendingMultipart = false, }); @override State createState() => _MessageBubbleState(); } class _MessageBubbleState extends State with SingleTickerProviderStateMixin { bool _showRaw = false; late AnimationController _animationController; late Animation _slideAnimation; late Animation _fadeAnimation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); _fadeAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeIn, ); // Directional Slide: Received from left, Sent from right final double startOffsetX = widget.isMe ? 0.3 : -0.3; _slideAnimation = Tween( begin: Offset(startOffsetX, 0), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.elasticOut, // Added premium bounce effect )); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } bool get _hasRawView => widget.rawBody != null && widget.rawBody!.isNotEmpty && widget.rawBody != widget.body; String get _displayBody { if (_showRaw && widget.rawBody != null) { return widget.rawBody!; } // Handle the payload separator if (widget.body.contains(' ::PAYLOAD::')) { return widget.body.split(' ::PAYLOAD::')[0]; } if (widget.body.contains(' | ')) { return widget.body.split(' | ')[0]; } return widget.body; } @override Widget build(BuildContext context) { final bool isLocked = widget.canRetryDecryption && !widget.isMe; // Premium Gradient logic for Sent messages final List sentGradients = widget.status == MessageStatus.failed ? [Colors.redAccent, Colors.red.shade900] : (widget.isSecure ? [const Color(0xFF6A11CB), const Color(0xFF2575FC), const Color(0xFF00D2FF)] // Secure: Violet to Cyan : [const Color(0xFF7000FF), const Color(0xFF5C6BC0)]); // Normal: Deep Purple to Indigo // Received background colors final Color receivedBg = isLocked ? const Color(0xFF0A192F) // Deep Blue Glass for locked : (widget.isSecure ? const Color(0xFF0D0D15) : const Color(0xFF0F0F0F)); return FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: ScaleTransition( scale: CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.7, curve: Curves.easeOutBack), ), child: Row( textDirection: TextDirection.ltr, mainAxisAlignment: widget.isMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only( left: widget.isMe ? 50 : 8, right: widget.isMe ? 8 : 50, top: 6, bottom: 6, ), child: CustomPaint( painter: BubblePainter( color: widget.isMe ? null : receivedBg, gradient: widget.isMe ? LinearGradient( colors: sentGradients, begin: Alignment.topLeft, end: Alignment.bottomRight, stops: sentGradients.length == 3 ? [0.0, 0.5, 1.0] : null, ) : (!widget.isMe && widget.isSecure && !isLocked ? const LinearGradient( colors: [Color(0xFF1A1A2E), Color(0xFF16213E)], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : null), isMe: widget.isMe, isSecure: widget.isSecure, ), child: Container( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.78), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (widget.isSecure) Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isLocked ? Icons.security_rounded : Icons.verified_user_rounded, size: 14, color: widget.isMe ? Colors.white70 : (isLocked ? const Color(0xFF00E5FF) : const Color(0xFF00D2FF)), ), const SizedBox(width: 6), Expanded( child: Text( isLocked ? "پیام رمزگذاری شده (نیاز به کلید)" : (widget.isPendingMultipart ? "در حال دریافت قطعات پیام..." : (widget.packetMode == 'SYM' ? "رمزنگاری متقارن (AES-256)" : (widget.packetMode == 'AE' ? "رمزنگاری نامتقارن (ECC)" : "پیام امن تایید شده"))), style: TextStyle( fontWeight: FontWeight.bold, color: widget.isMe ? Colors.white.withValues(alpha: 0.8) : (isLocked ? const Color(0xFF00E5FF) : const Color(0xFF00D2FF)), fontSize: 10, letterSpacing: 0.2, ), ), ), ], ), ), if (widget.isPendingMultipart) Shimmer.fromColors( baseColor: Colors.white.withValues(alpha: 0.1), highlightColor: Colors.white.withValues(alpha: 0.3), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(height: 14, width: double.infinity, decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(4))), const SizedBox(height: 6), Container(height: 14, width: 150, decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(4))), ], ), ) else Directionality( textDirection: TextDirection.rtl, child: Text( _displayBody, style: TextStyle( color: widget.isMe ? Colors.white : Colors.white.withValues(alpha: 0.95), fontSize: 15.5, fontWeight: isLocked ? FontWeight.w700 : FontWeight.w400, height: 1.5, fontFamily: _showRaw ? "monospace" : null, ), ), ), if (isLocked) ...[ const SizedBox(height: 12), InkWell( onTap: widget.onRetryDecryption, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: const Color(0xFF00E5FF).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF00E5FF).withValues(alpha: 0.3), width: 1), ), child: const Row( children: [ Icon(Icons.vpn_key_rounded, size: 18, color: Color(0xFF00E5FF)), SizedBox(width: 10), Expanded( child: Text( "برای بازگشایی ضربه بزنید", style: TextStyle(fontSize: 12, color: Color(0xFF00E5FF), fontWeight: FontWeight.bold), ), ), Icon(Icons.chevron_right_rounded, color: Color(0xFF00E5FF)), ], ), ), ), ], const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (widget.isSecure && _hasRawView) Padding( padding: const EdgeInsets.only(right: 8), child: InkWell( onTap: () => setState(() => _showRaw = !_showRaw), child: Text( _showRaw ? "متن اصلی" : "دیتای خام", style: TextStyle( fontSize: 9, fontWeight: FontWeight.bold, color: widget.isMe ? Colors.white70 : const Color(0xFF00D2FF), ), ), ), ), if (widget.statusLabel != null) Flexible( child: Text( widget.statusLabel!, style: TextStyle(fontSize: 9, color: widget.isMe ? Colors.white60 : Colors.white38), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), Text( _formatTime(widget.date), style: TextStyle(fontSize: 10, color: widget.isMe ? Colors.white60 : Colors.white38), ), if (widget.isMe) ...[ const SizedBox(width: 4), _buildStatusIcon(), ] ], ), ], ), ), ), ), ], ), ), ), ); } Widget _buildStatusIcon() { switch (widget.status) { case MessageStatus.sending: return const SizedBox( width: 8, height: 8, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 1, ), ); case MessageStatus.sent: return const Icon(Icons.done_all, size: 12, color: Colors.white70); case MessageStatus.failed: return const Icon(Icons.error_outline, size: 12, color: Colors.white); default: return const SizedBox(); } } String _formatTime(int millis) { final d = DateTime.fromMillisecondsSinceEpoch(millis); return "${d.hour}:${d.minute.toString().padLeft(2, '0')}"; } } class BubblePainter extends CustomPainter { final Color? color; final Gradient? gradient; final bool isMe; final bool isSecure; BubblePainter({ this.color, this.gradient, required this.isMe, this.isSecure = false, }); @override void paint(Canvas canvas, Size size) { final Paint paint = Paint()..style = PaintingStyle.fill; const double tailWidth = 10.0; const double tailHeight = 12.0; if (gradient != null) { paint.shader = gradient!.createShader(Rect.fromLTWH(0, 0, size.width, size.height)); } else if (color != null) { paint.color = color!; } const double R = 22.0; // Standard radius final Path path = Path(); if (isMe) { // Sent Message: Tail on bottom-right path.moveTo(R, 0); path.lineTo(size.width - R, 0); path.quadraticBezierTo(size.width, 0, size.width, R); path.lineTo(size.width, size.height - tailHeight - R); path.quadraticBezierTo(size.width, size.height - tailHeight, size.width - tailWidth, size.height - tailHeight); path.lineTo(size.width, size.height); // Pointy tail tip path.lineTo(size.width - tailWidth - R, size.height - tailHeight); path.lineTo(R, size.height - tailHeight); path.quadraticBezierTo(0, size.height - tailHeight, 0, size.height - tailHeight - R); path.lineTo(0, R); path.quadraticBezierTo(0, 0, R, 0); path.close(); } else { // Received Message: Tail on bottom-left path.moveTo(tailWidth + R, 0); path.lineTo(size.width - R, 0); path.quadraticBezierTo(size.width, 0, size.width, R); path.lineTo(size.width, size.height - tailHeight - R); path.quadraticBezierTo(size.width, size.height - tailHeight, size.width - R, size.height - tailHeight); path.lineTo(tailWidth + R, size.height - tailHeight); path.lineTo(0, size.height); // Pointy tail tip path.lineTo(tailWidth, size.height - tailHeight); path.lineTo(tailWidth, R); path.quadraticBezierTo(tailWidth, 0, tailWidth + R, 0); path.close(); } // --- Enhanced Glowing Shadows --- if (isSecure) { final Color shadowColor = isMe ? const Color(0xFF7000FF) : const Color(0xFF00D2FF); // Layered glow effect for (int i = 1; i <= 3; i++) { canvas.drawShadow( path.shift(Offset(0, 1.0 * i)), shadowColor.withValues(alpha: 0.15 / i), 4.0 * i, false, ); } } else { canvas.drawShadow( path.shift(const Offset(0, 3)), Colors.black.withValues(alpha: 0.4), 6.0, false, ); } canvas.drawPath(path, paint); // --- Subtle Inner Border for Glass Effect --- if (!isMe) { final Paint borderPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 0.5 ..color = Colors.white.withValues(alpha: 0.15); canvas.drawPath(path, borderPaint); } } @override bool shouldRepaint(covariant BubblePainter oldDelegate) => oldDelegate.color != color || oldDelegate.gradient != gradient || oldDelegate.isMe != isMe || oldDelegate.isSecure != isSecure; } // Removed incorrect ColorExt