import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/chat_service.dart'; import '../services/reverb_socket_service.dart'; import '../services/dio_client.dart'; import '../providers/chat_unread_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:io'; import 'package:file_selector/file_selector.dart'; import 'chat_file_viewer.dart'; import '../widgets/chat_file_preview.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { final TextEditingController _messageCtrl = TextEditingController(); final ScrollController _scrollCtrl = ScrollController(); Map? uploadingMessage; late ChatService _chatService; final ReverbSocketService _socket = ReverbSocketService(); int? ticketId; List> messages = []; bool isLoading = true; // ============================ // INIT STATE // ============================ @override void initState() { super.initState(); _chatService = ChatService(DioClient.getInstance(context)); // 🔔 Mark chat as OPEN (important) WidgetsBinding.instance.addPostFrameCallback((_) { context.read().setChatOpen(true); }); _initChat(); } String _guessMimeType(String path) { final lower = path.toLowerCase(); if (lower.endsWith('.jpg') || lower.endsWith('.png')) return 'image/*'; if (lower.endsWith('.mp4')) return 'video/*'; if (lower.endsWith('.pdf')) return 'application/pdf'; return 'application/octet-stream'; } Future _pickAndSendFile() async { final XFile? picked = await openFile(); if (picked == null || ticketId == null) return; final file = File(picked.path); // 1️⃣ Show uploading UI setState(() { uploadingMessage = { 'local_file': file, 'file_type': _guessMimeType(file.path), 'progress': 0.0, }; }); // 2️⃣ Upload (NO adding message) await _chatService.sendFile( ticketId!, file, onProgress: (progress) { if (!mounted) return; setState(() { uploadingMessage!['progress'] = progress; }); }, ); // 3️⃣ Remove sending bubble ONLY if (!mounted) return; setState(() { uploadingMessage = null; }); // 🚫 DO NOT add message here // WebSocket will handle it } // ============================ // INIT CHAT // ============================ Future _initChat() async { // 1️⃣ Start chat final ticketRes = await _chatService.startChat(); ticketId = ticketRes['ticket']['id']; // 2️⃣ Load messages final msgs = await _chatService.getMessages(ticketId!); messages = List>.from(msgs); // 3️⃣ Realtime socket await _socket.connect( context: context, ticketId: ticketId!, onMessage: (msg) { final incomingClientId = msg['client_id']; setState(() { // 🧹 Remove local temp message with same client_id messages.removeWhere( (m) => m['client_id'] != null && m['client_id'] == incomingClientId, ); // ✅ Add confirmed socket message messages.add(msg); }); _scrollToBottom(); }, onAdminMessage: () { if (!mounted) { context.read().increment(); } }, ); if (!mounted) return; setState(() => isLoading = false); _scrollToBottom(); } // ============================ // SCROLL // ============================ void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollCtrl.hasClients) { _scrollCtrl.jumpTo(_scrollCtrl.position.maxScrollExtent); } }); } // ============================ // SEND MESSAGE // ============================ Future _sendMessage() async { final text = _messageCtrl.text.trim(); if (text.isEmpty || ticketId == null) return; _messageCtrl.clear(); final clientId = DateTime.now().millisecondsSinceEpoch.toString(); // 1️⃣ ADD LOCAL MESSAGE IMMEDIATELY setState(() { messages.add({ 'client_id': clientId, 'sender_type': 'App\\Models\\User', 'message': text, 'file_path': null, 'file_type': null, 'sending': true, }); }); _scrollToBottom(); // 2️⃣ SEND TO SERVER await _chatService.sendMessage( ticketId!, message: text, clientId: clientId, ); } // ============================ // DISPOSE // ============================ @override void dispose() { // 🔕 Mark chat CLOSED context.read().setChatOpen(false); _socket.disconnect(); _messageCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } // ============================ // UI // ============================ @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Support Chat")), body: isLoading ? const Center(child: CircularProgressIndicator()) : Column( children: [ Expanded(child: _buildMessages()), _buildInput(), ], ), ); } // Future _openUrl(String url) async { // final uri = Uri.parse(url); // // if (await canLaunchUrl(uri)) { // await launchUrl( // uri, // mode: LaunchMode.externalApplication, // ); // } else { // debugPrint("❌ Cannot launch URL: $url"); // } // } Widget _buildMessageContent({ String? message, String? filePath, String? fileType, required bool isUser, }) { final textColor = isUser ? Colors.white : Colors.black; if (filePath == null) { return Text(message ?? '', style: TextStyle(color: textColor)); } final url = "${DioClient.baseUrl}/storage/$filePath"; return GestureDetector( onTap: () { ChatFileViewer.open( context, url: url, fileType: fileType ?? '', ); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(_iconForFile(fileType), color: textColor), const SizedBox(width: 8), Text( _labelForFile(fileType), style: TextStyle(color: textColor), ), ], ), ); } IconData _iconForFile(String? type) { if (type == null) return Icons.insert_drive_file; if (type.startsWith('image/')) return Icons.image; if (type.startsWith('video/')) return Icons.play_circle_fill; if (type == 'application/pdf') return Icons.picture_as_pdf; return Icons.insert_drive_file; } String _labelForFile(String? type) { if (type == null) return "File"; if (type.startsWith('image/')) return "Image"; if (type.startsWith('video/')) return "Video"; if (type == 'application/pdf') return "PDF"; return "Download file"; } Widget _buildMessages() { return ListView( controller: _scrollCtrl, padding: const EdgeInsets.all(12), children: [ // EXISTING MESSAGES ...messages.map((msg) { final isUser = msg['sender_type'] == 'App\\Models\\User'; return Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isUser ? Colors.blue : Colors.grey.shade300, borderRadius: BorderRadius.circular(12), ), child: msg['file_path'] == null ? Text( msg['message'] ?? '', style: TextStyle( color: isUser ? Colors.white : Colors.black, ), ) : ChatFilePreview( filePath: msg['file_path'], fileType: msg['file_type'] ?? '', isUser: isUser, ), ), ); }), // ⏳ UPLOADING MESSAGE if (uploadingMessage != null) Align( alignment: Alignment.centerRight, child: Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ChatFilePreview( filePath: uploadingMessage!['local_file'].path, fileType: uploadingMessage!['file_type'], isUser: true, isLocal: true, ), const SizedBox(height: 8), LinearProgressIndicator( value: uploadingMessage!['progress'], backgroundColor: Colors.white24, valueColor: const AlwaysStoppedAnimation(Colors.white), ), const SizedBox(height: 4), const Text( "Sending…", style: TextStyle( color: Colors.white, fontSize: 12, ), ), ], ), ), ), ], ); } Widget _buildInput() { return SafeArea( child: Row( children: [ IconButton( icon: const Icon(Icons.attach_file), onPressed: _pickAndSendFile, ), Expanded( child: TextField( controller: _messageCtrl, decoration: const InputDecoration( hintText: "Type message", border: InputBorder.none, ), ), ), IconButton( icon: const Icon(Icons.send), onPressed: _sendMessage, ), ], ), ); } }