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:get/get_core/src/get_main.dart'; import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/app_style.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/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 CompletedLiveChatScreen extends ConsumerStatefulWidget { final int chatid; final String? fileid; const CompletedLiveChatScreen({ super.key, required this.chatid, required this.fileid, }); @override ConsumerState createState() => _CompletedLiveChatScreenState(); } class _CompletedLiveChatScreenState extends ConsumerState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { bool get wantKeepAlive => true; final ScrollController scrollController = ScrollController(); final ChatWebSocketService _wsService = ChatWebSocketService(); bool showScrollButton = false; bool userIsReading = false; bool _isInitialLoad = true; bool _isPaginationLoading = false; bool _hasNewMessage = false; // 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); _initializeWebSocket(); scrollController.addListener(_handleScroll); } // ⭐⭐⭐ Enhanced scroll handler with early pagination trigger void _handleScroll() { if (!scrollController.hasClients) return; double position = scrollController.position.pixels; double bottom = scrollController.position.maxScrollExtent; double top = scrollController.position.minScrollExtent; // ⭐⭐⭐ EARLY PAGINATION: Load before reaching exact top if (position <= top + _paginationTriggerOffset && !_isPaginationLoading) { _triggerPagination(); } // Show scroll to bottom button if (position < bottom - 50) { if (!userIsReading) { setState(() { userIsReading = true; showScrollButton = true; }); } } else { if (userIsReading) { setState(() { userIsReading = false; showScrollButton = false; }); } } } // ⭐⭐⭐ Smooth pagination with scroll position preservation (NO JUMP TO BOTTOM) Future _triggerPagination() async { final notifier = ref.read(chatMessagesProvider(widget.chatid).notifier); if (!notifier.hasNextPage) return; if (_isPaginationLoading) return; if (!scrollController.hasClients) return; setState(() { _isPaginationLoading = true; // userIsReading remains true while scrolling old messages }); // Save current scroll metrics BEFORE loading more messages final double beforeMaxExtent = scrollController.position.maxScrollExtent; final double currentOffset = scrollController.position.pixels; try { await notifier.loadMessages(); // This should prepend older messages // Restore scroll position after new messages loaded WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || !scrollController.hasClients) return; final double afterMaxExtent = scrollController.position.maxScrollExtent; final double diff = afterMaxExtent - beforeMaxExtent; // Keep user at same visual message position final double newOffset = currentOffset + diff; scrollController.jumpTo( newOffset.clamp( scrollController.position.minScrollExtent, scrollController.position.maxScrollExtent, ), ); if (mounted) { setState(() { _isPaginationLoading = false; }); } }); } catch (e) { debugPrint("❌ Pagination error: $e"); 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"); // Only auto-scroll to bottom on new messages if user is NOT reading old ones if (!userIsReading && mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { scrollController.animateTo( scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } } 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; } } } 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() { WidgetsBinding.instance.removeObserver(this); _wsService.disconnect(); scrollController.dispose(); super.dispose(); } void scrollToBottom({bool animated = false}) { if (!scrollController.hasClients) return; setState(() { _isPaginationLoading = false; userIsReading = false; }); WidgetsBinding.instance.addPostFrameCallback((_) { if (!scrollController.hasClients) return; if (animated) { scrollController .animateTo( scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic, ) .then((_) { Future.delayed(const Duration(milliseconds: 50), () { if (scrollController.hasClients && mounted) { scrollController.jumpTo( scrollController.position.maxScrollExtent, ); } }); }); } else { scrollController.jumpTo(scrollController.position.maxScrollExtent); Future.delayed(const Duration(milliseconds: 10), () { if (scrollController.hasClients && mounted) { scrollController.jumpTo(scrollController.position.maxScrollExtent); } }); } }); } // ⭐ FIXED: Use unique string key instead of int void 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, ); } } // ⭐ FIXED: Generate unique string key for each message GlobalKey _getKeyForMessage(String messageKey) { if (!_messageKeys.containsKey(messageKey)) { _messageKeys[messageKey] = GlobalKey(); } return _messageKeys[messageKey]!; } // ⭐ FIXED: Generate unique key string from message String _getMessageKeyString(MessageModel msg, int index) { // Use combination of ID, index, and document count for uniqueness final id = msg.id; final docCount = msg.uploadedDocuments.length; final createdAt = msg.createdAt ?? ''; return 'msg_${id}_${index}_${docCount}_$createdAt'; } 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 final isRead = message.isRead; final isDelivered = message.isDelivered; 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); } } @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: AppTextStyles.semiBold.copyWith( fontSize: 16, color: Colors.black, ), ), backgroundColor: Colors.white, elevation: 1, foregroundColor: Colors.black, centerTitle: true, actions: [ Padding( padding: const EdgeInsets.only(right: 12), // ✅ Right end spacing 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) { // ✅ Only first load scrolls to bottom, NOT during pagination / scroll up if (_isInitialLoad && !_isPaginationLoading) { WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients && mounted) { scrollController.jumpTo( scrollController.position.maxScrollExtent, ); } }); _isInitialLoad = false; } return Stack( children: [ ListView.builder( controller: scrollController, physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only( left: 15, right: 15, top: 15, bottom: 15, ), itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[index]; final msgDate = DateTime.parse(msg.createdAt!); final showDateHeader = index == 0 || formatMessageDate( DateTime.parse( messages[index - 1].createdAt!, ), ) != formatMessageDate(msgDate); // ⭐ FIXED: Generate unique key string final messageKeyString = _getMessageKeyString( msg, index, ); return Column( key: _getKeyForMessage(messageKeyString), 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) { // ⭐ FIXED: Find the message key string for the tagId final tagIndex = messages.indexWhere( (m) => m.id == tagId, ); if (tagIndex != -1) { final taggedMsg = messages[tagIndex]; final tagKeyString = _getMessageKeyString( taggedMsg, tagIndex, ); scrollToMessage(tagKeyString); } } }, // ⭐ FIXED: Use messageKeyString as ValueKey key: ValueKey(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) { final tagIndex = messages.indexWhere( (m) => m.id == taggedMessage.id, ); if (tagIndex != -1) { final tagKeyString = _getMessageKeyString( taggedMessage, tagIndex, ); scrollToMessage(tagKeyString); } }); } }, 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 (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), ), ), ], ), ); } }