import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; import 'package:get/get.dart'; import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/notification_webscoket.dart'; import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/model/chat_model.dart'; import 'package:taxglide/view/Mahi_chat/chat_profile_screen.dart'; import 'package:taxglide/view/Mahi_chat/comman_input_button.dart'; import 'package:taxglide/view/Mahi_chat/file_images_screen.dart'; import 'package:taxglide/view/Mahi_chat/webscoket.dart'; // ⭐ Provider to hold the tagged message final taggedMessageProvider = StateProvider((ref) => null); // ⭐ Global key map to track message positions (if you want from outside) final messageKeysProvider = StateProvider>((ref) => {}); class LiveChatScreen extends ConsumerStatefulWidget { final int chatid; final String? fileid; const LiveChatScreen({super.key, required this.chatid, required this.fileid}); @override ConsumerState createState() => _LiveChatScreenState(); } class _LiveChatScreenState extends ConsumerState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { @override bool get wantKeepAlive => true; final ScrollController scrollController = ScrollController(); final ChatWebSocketService _wsService = ChatWebSocketService(); bool showScrollButton = false; bool userIsReading = false; bool _isPaginationLoading = false; bool _hasNewMessage = false; bool _isOtherUserTyping = false; Timer? _typingTimeoutTimer; // For reply scroll - FIXED: Use unique key per message final Map _messageKeys = {}; // ⭐⭐⭐ Key variables for smooth pagination final double _paginationTriggerOffset = 300; // Start loading 300px before top @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // ✅ Tell the global notification WS that we are viewing this chat, // so it suppresses banner notifications for this specific chat. NotificationWebSocket().setActiveChatId(widget.chatid.toString()); _initializeWebSocket(); scrollController.addListener(_handleScroll); // Refresh immediately in background if already loaded Future.microtask(() { final currentState = ref.read(chatMessagesProvider(widget.chatid)); if (currentState is AsyncData) { ref.read(chatMessagesProvider(widget.chatid).notifier).refresh(); } }); } // ⭐ WhatsApp-like Smooth Pagination Logic void _handleScroll() { if (!scrollController.hasClients) return; double position = scrollController.position.pixels; double maxExtent = scrollController.position.maxScrollExtent; // In a reversed list, maxExtent is the "top" (older messages) if (position >= maxExtent - _paginationTriggerOffset && !_isPaginationLoading) { _triggerPagination(); } // Show scroll to bottom button // In a reversed list, bottom is 0 if (position > 100) { if (!showScrollButton) { setState(() => showScrollButton = true); } } else { if (showScrollButton) { setState(() => showScrollButton = false); } } } // ⭐ Simplified pagination for reversed list Future _triggerPagination() async { final notifier = ref.read(chatMessagesProvider(widget.chatid).notifier); if (!notifier.hasNextPage || _isPaginationLoading) return; setState(() => _isPaginationLoading = true); try { await notifier.loadMessages(); // Prepends older messages naturally } catch (e) { debugPrint("❌ Pagination error: $e"); } finally { if (mounted) setState(() => _isPaginationLoading = false); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); debugPrint("📱 App Lifecycle State: $state"); if (state == AppLifecycleState.resumed) { debugPrint("🔄 App resumed - checking for new messages"); if (_hasNewMessage) { _silentRefresh(); _hasNewMessage = false; } } } Future _silentRefresh() async { try { debugPrint("🔄 Starting silent refresh..."); await ref.read(chatMessagesProvider(widget.chatid).notifier).refresh(); debugPrint("✅ Silent refresh completed"); // In a reversed list, new messages are at 0. No manual scroll needed. // The reverse: true property handles this naturally. if (mounted) { setState(() {}); } } catch (e) { debugPrint("❌ Error during silent refresh: $e"); } } Future _initializeWebSocket() async { try { _wsService.onMessageReceived = (Map message) async { debugPrint("📨 WebSocket message received: $message"); try { final event = message['event'] as String?; if (event == 'message.sent' || event == 'message.received' || event == 'message.read' || event == 'message.delivered') { debugPrint("🔔 Message status/new message event: $event"); if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { debugPrint("✅ Screen active - refreshing now"); await _silentRefresh(); } else { debugPrint("⏳ Screen not active - will refresh on resume"); _hasNewMessage = true; } // ℹ️ Notification banner is handled centrally in NotificationWebSocket. // No need to show it here – it is already suppressed for this chat. } if (event == 'client-typing') { debugPrint("⌨️ Typing event received: ${message['data']}"); _handleTypingEvent(message['data']); } } catch (e) { debugPrint("❌ Error handling WebSocket message: $e"); } }; _wsService.onConnected = () { debugPrint("✅ WebSocket Connected"); }; _wsService.onDisconnected = () { debugPrint("🔌 WebSocket Disconnected"); }; _wsService.onError = (error) { debugPrint("❌ WebSocket Error: $error"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("Connection error: $error"), backgroundColor: Colors.red, duration: const Duration(seconds: 2), ), ); } }; await _wsService.connect(chatId: widget.chatid.toString()); } catch (e) { debugPrint("❌ Error initializing WebSocket: $e"); } } @override void dispose() { // ✅ Clear active chat so notifications resume when user leaves this screen. NotificationWebSocket().clearActiveChatId(); WidgetsBinding.instance.removeObserver(this); _wsService.disconnect(); scrollController.dispose(); super.dispose(); } void _handleTypingEvent(dynamic data) { try { Map dataMap; if (data is String) { dataMap = json.decode(data); } else if (data is Map) { dataMap = Map.from(data); } else { return; } final bool isTyping = dataMap['is_typing'] == true; setState(() { _isOtherUserTyping = isTyping; }); // Auto-clear typing status after 5 seconds of no update _typingTimeoutTimer?.cancel(); if (_isOtherUserTyping) { _typingTimeoutTimer = Timer(const Duration(seconds: 5), () { if (mounted) { setState(() { _isOtherUserTyping = false; }); } }); } } catch (e) { debugPrint("❌ Error parsing typing data: $e"); } } void scrollToBottom({bool animated = false}) { // In a reversed list, bottom is pixels: 0 if (!scrollController.hasClients) return; if (animated) { scrollController.animateTo( 0, duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic, ); } else { scrollController.jumpTo(0); } } // ⭐ FIXED: Use stable key that doesn't change with pagination bool scrollToMessage(String messageKey) { final key = _messageKeys[messageKey]; if (key != null && key.currentContext != null) { Scrollable.ensureVisible( key.currentContext!, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, alignment: 0.3, ); // Trigger highlight animation if (key is GlobalKey) { key.currentState?.highlight(); } else { final state = key.currentContext! .findAncestorStateOfType(); state?.highlight(); } return true; } return false; } // ⭐ NEW: Search and scroll to message that might not be loaded yet Future _scrollToMissingMessage(int tagId) async { final notifier = ref.read(chatMessagesProvider(widget.chatid).notifier); // Check if it exists in locally loaded messages first final currentMessages = ref.read(chatMessagesProvider(widget.chatid)).value ?? []; final existingIndex = currentMessages.indexWhere((m) => m.id == tagId); if (existingIndex != -1) { final targetMsg = currentMessages[existingIndex]; final keyString = _getMessageKeyString(targetMsg); // 1. Try direct scroll if (scrollToMessage(keyString)) return; // 2. If not rendered, perform a smooth animation to force it final reversedIndex = (currentMessages.length - 1 - existingIndex); final targetOffset = reversedIndex * 150.0; // Estimated position await scrollController.animateTo( targetOffset, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); await Future.delayed(const Duration(milliseconds: 100)); scrollToMessage(keyString); return; } // --- If NOT in memory, paginate silently (no full-screen loader) --- try { bool found = false; int maxRetries = 20; while (!found && maxRetries > 0) { final messages = ref.read(chatMessagesProvider(widget.chatid)).value ?? []; final index = messages.indexWhere((m) => m.id == tagId); if (index != -1) { final targetMsg = messages[index]; final keyString = _getMessageKeyString(targetMsg); final reversedIndex = (messages.length - 1 - index); final targetOffset = reversedIndex * 150.0; await scrollController.animateTo( targetOffset, duration: const Duration(milliseconds: 400), curve: Curves.easeOut, ); // Retry highlight after animation for (int retry = 0; retry < 3; retry++) { await Future.delayed(Duration(milliseconds: 100 + (retry * 100))); if (scrollToMessage(keyString)) { found = true; break; } } if (found) break; } if (!notifier.hasNextPage) break; await notifier.loadMessages(); maxRetries--; } if (!found && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Could not find the original message")), ); } } catch (e) { debugPrint("❌ Error searching for message: $e"); } } // ⭐ FIXED: Generate unique string key for each message GlobalKey _getKeyForMessage(String messageKey) { if (!_messageKeys.containsKey(messageKey)) { _messageKeys[messageKey] = GlobalKey(); } return _messageKeys[messageKey] as GlobalKey; } // ⭐ FIXED: Generate stable key from message data String _getMessageKeyString(MessageModel msg) { // Unique features: ID, type, and creation time final id = msg.id; final createdAt = msg.createdAt ?? ''; final message = msg.message.hashCode; // Add message hash for extra uniqueness return 'msg_${id}_${createdAt}_$message'; } String formatMessageDate(DateTime date) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final msgDay = DateTime(date.year, date.month, date.day); if (msgDay == today) return "Today"; if (msgDay == today.subtract(const Duration(days: 1))) return "Yesterday"; return "${date.day}-${date.month}-${date.year}"; } String formatTime(DateTime time) { final hour = time.hour % 12 == 0 ? 12 : time.hour % 12; final minute = time.minute.toString().padLeft(2, '0'); final period = time.hour >= 12 ? "PM" : "AM"; return "$hour:$minute $period"; } Widget buildTick(MessageModel message) { // ⭐ Status 0/1/pending final isRead = message.isRead; final isDelivered = message.isDelivered; // Check if it's an optimistic pending message (huge ID) if (message.id > 1000000000000) { return const Icon(Icons.schedule, size: 14, color: Colors.grey); } if (isRead == 1) { return const Icon(Icons.done_all, size: 16, color: Colors.blue); } else if (isDelivered == 1) { return const Icon(Icons.done_all, size: 16, color: Colors.grey); } else { return const Icon(Icons.check, size: 16, color: Colors.grey); } } // 🖼️ NEW: Show Image Grid Dialog void _showImageGridDialog(List imageUrls, int initialIndex) { showDialog( context: context, barrierColor: Colors.black87, builder: (context) => ImageGridViewer(imageUrls: imageUrls, initialIndex: initialIndex), ); } @override Widget build(BuildContext context) { super.build(context); final chatAsync = ref.watch(chatMessagesProvider(widget.chatid)); final taggedMessage = ref.watch(taggedMessageProvider); return Scaffold( appBar: AppBar( title: Text( (widget.fileid != null && widget.fileid.toString().isNotEmpty && widget.fileid.toString() != "0") ? 'File ID : ${widget.fileid.toString()}' : "Live Chat", style: const TextStyle( fontFamily: "Gilroy", fontWeight: FontWeight.w600, ), ), backgroundColor: Colors.white, elevation: 1, foregroundColor: Colors.black, centerTitle: true, actions: [ Padding( padding: const EdgeInsets.only(right: 12), child: GestureDetector( onTap: () { Get.to(ChatProfileScreen(chatid: widget.chatid)); }, child: Container( width: 37.446807861328125, height: 40, decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(6.81), border: Border.all( color: const Color(0xFF1E780C), width: 0.85, ), ), child: const Icon(Icons.person, color: Color(0xFF1E780C)), ), ), ), ], ), body: Stack( children: [ Container( decoration: const BoxDecoration( image: DecorationImage( image: AssetImage(AppAssets.backgroundimages), fit: BoxFit.cover, ), ), ), Column( children: [ Expanded( child: chatAsync.when( loading: () => const Center(child: SizedBox.shrink()), error: (e, _) => Center(child: Text(e.toString())), data: (messages) { final seenKeys = {}; final uniqueMessages = messages.where((msg) { final key = _getMessageKeyString(msg); if (seenKeys.contains(key)) return false; seenKeys.add(key); return true; }).toList(); final reversedMessages = uniqueMessages.reversed.toList(); return Stack( children: [ ListView.builder( controller: scrollController, reverse: true, // ⭐ Index 0 is now at the bottom physics: const ClampingScrollPhysics(), cacheExtent: 1000, // ⭐ Help with deep scrolling visibility padding: const EdgeInsets.only( left: 15, right: 15, top: 15, bottom: 15, ), itemCount: reversedMessages.length, itemBuilder: (context, index) { final msg = reversedMessages[index]; final msgDate = DateTime.parse(msg.createdAt!); // In reversed list, header shows if previous message (index+1) has different date final showDateHeader = index == reversedMessages.length - 1 || formatMessageDate( DateTime.parse( reversedMessages[index + 1].createdAt!, ), ) != formatMessageDate(msgDate); // ⭐ FIXED: Use stable key (no index dependency) final messageKeyString = _getMessageKeyString(msg); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (showDateHeader) Padding( padding: const EdgeInsets.symmetric( vertical: 10, ), child: Center( child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 5, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.06), borderRadius: BorderRadius.circular( 12, ), ), child: Text( formatMessageDate(msgDate), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ), ), SwipeableMessageBubble( message: msg, msgDate: msgDate, formatTime: formatTime, buildTick: buildTick, onSwipe: () { ref .read( taggedMessageProvider.notifier, ) .state = msg; }, onParentTagTap: (tagId) { if (tagId == null) return; // ALWAYS call _scrollToMissingMessage now as it handles both // local smooth scroll and remote paginate search correctly. _scrollToMissingMessage(tagId); }, // 🖼️ NEW: Pass image grid callback onImageTap: (imageUrls, initialIndex) { _showImageGridDialog( imageUrls, initialIndex, ); }, key: _getKeyForMessage(messageKeyString), ), ], ); }, ), // ⭐⭐⭐ Subtle loading indicator at top if (_isPaginationLoading) Positioned( top: 5, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, ), ), SizedBox(width: 8), Text( "Loading...", style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), ], ); }, ), ), if (taggedMessage != null) GestureDetector( onTap: () { if (taggedMessage != null) { // ⭐ FIXED: Need to find the message in the list to get proper key final chatAsync = ref.read( chatMessagesProvider(widget.chatid), ); chatAsync.whenData((messages) { _scrollToMissingMessage(taggedMessage.id); }); } }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 8, ), decoration: BoxDecoration( color: Colors.grey[200], border: Border(top: BorderSide(color: Colors.grey[300]!)), ), child: Row( children: [ Container( width: 4, height: 50, decoration: BoxDecoration( color: const Color(0xFF630A73), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( taggedMessage.user?.name ?? "Unknown", style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 13, color: Color(0xFF630A73), ), ), const SizedBox(height: 2), if (taggedMessage.type == "file" && taggedMessage.fileName != null) Row( children: [ Icon( Icons.attach_file, size: 14, color: Colors.grey[600], ), const SizedBox(width: 4), Expanded( child: Text( taggedMessage.fileName!, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: Colors.grey[700], ), ), ), ], ) else Text( taggedMessage.message, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: Colors.grey[700], ), ), ], ), ), IconButton( icon: const Icon(Icons.close, size: 20), padding: EdgeInsets.zero, onPressed: () { ref.read(taggedMessageProvider.notifier).state = null; }, ), ], ), ), ), if (_isOtherUserTyping) Padding( padding: const EdgeInsets.only(left: 20, bottom: 5), child: Row( children: [ const SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF1E780C), ), ), const SizedBox(width: 8), Text( "Typing...", style: TextStyle( fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ), ), ChatInputBox( widget.chatid, onMessageSent: () { setState(() { userIsReading = false; }); scrollToBottom(animated: true); }, ), ], ), if (showScrollButton) Positioned( bottom: 150, right: 20, child: FloatingActionButton( heroTag: "scrollBtn", backgroundColor: Colors.green, mini: true, onPressed: () { scrollToBottom(animated: true); }, child: const Icon(Icons.arrow_downward, color: Colors.white), ), ), ], ), ); } } // 🖼️ NEW: Image Grid Viewer Widget class ImageGridViewer extends StatefulWidget { final List imageUrls; final int initialIndex; const ImageGridViewer({ Key? key, required this.imageUrls, required this.initialIndex, }) : super(key: key); @override State createState() => _ImageGridViewerState(); } class _ImageGridViewerState extends State { late PageController _pageController; late int _currentIndex; @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _pageController = PageController(initialPage: widget.initialIndex); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.transparent, insetPadding: EdgeInsets.zero, child: Container( width: double.infinity, height: double.infinity, color: Colors.black87, child: Stack( children: [ // Main Image Viewer PageView.builder( controller: _pageController, onPageChanged: (index) { setState(() { _currentIndex = index; }); }, itemCount: widget.imageUrls.length, itemBuilder: (context, index) { return Center( child: InteractiveViewer( minScale: 0.5, maxScale: 4.0, child: Image.network( widget.imageUrls[index], fit: BoxFit.contain, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, color: Colors.white, ), ); }, errorBuilder: (context, error, stackTrace) { return const Center( child: Icon( Icons.broken_image, color: Colors.white54, size: 64, ), ); }, ), ), ); }, ), // Close Button Positioned( top: 40, right: 20, child: IconButton( icon: const Icon(Icons.close, color: Colors.white, size: 30), onPressed: () => Navigator.pop(context), ), ), // Image Counter Positioned( top: 50, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), ), child: Text( '${_currentIndex + 1} / ${widget.imageUrls.length}', style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), // Thumbnail Grid at Bottom Positioned( bottom: 20, left: 0, right: 0, child: Container( height: 80, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 20), itemCount: widget.imageUrls.length, itemBuilder: (context, index) { final isSelected = index == _currentIndex; return GestureDetector( onTap: () { _pageController.animateToPage( index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, child: Container( width: 112.8125, height: 66.88169860839844, margin: const EdgeInsets.only(right: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected ? Colors.white : Colors.transparent, width: 3, ), ), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( widget.imageUrls[index], fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( color: Colors.grey[800], child: const Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white54, ), ), ), ); }, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.grey[800], child: const Icon( Icons.broken_image, color: Colors.white54, size: 24, ), ); }, ), ), ), ); }, ), ), ), ], ), ), ); } }