import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:taxglide/consts/download_helper.dart'; import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/controller/api_repository.dart'; import 'package:taxglide/model/chat_model.dart'; import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart'; // ⭐⭐⭐ CHAT INPUT BOX WITH FILE TAG SUPPORT ⭐⭐⭐ class ChatInputBox extends ConsumerStatefulWidget { final int chatId; final VoidCallback? onMessageSent; const ChatInputBox(this.chatId, {Key? key, this.onMessageSent}) : super(key: key); @override ConsumerState createState() => _ChatInputBoxState(); } class _ChatInputBoxState extends ConsumerState { final TextEditingController messageCtrl = TextEditingController(); final ImagePicker picker = ImagePicker(); List selectedFiles = []; bool _isSending = false; @override void initState() { super.initState(); _requestPermissions(); } // ⭐ Request storage permissions Future _requestPermissions() async { await DownloadHelper.requestStoragePermission(); } void showPickerMenu() { showModalBottomSheet( context: context, backgroundColor: Colors.white, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(15)), ), builder: (context) { return SafeArea( bottom: true, child: SizedBox( height: 220, child: Column( children: [ const SizedBox(height: 10), ListTile( leading: const Icon(Icons.photo_camera, size: 28), title: const Text("Camera", style: TextStyle(fontSize: 17)), onTap: pickFromCamera, ), ListTile( leading: const Icon(Icons.photo_library, size: 28), title: const Text("Gallery", style: TextStyle(fontSize: 17)), onTap: pickFromGallery, ), ListTile( leading: const Icon(Icons.attach_file, size: 28), title: const Text("Files", style: TextStyle(fontSize: 17)), onTap: pickDocuments, ), const SizedBox(height: 10), ], ), ), ); }, ); } Future pickFromCamera() async { Navigator.pop(context); try { var status = await Permission.camera.status; // Handle cases where we cannot show the system dialog again // Skip strict blocks on iOS Simulator by just trying to launch picker if (!Platform.isIOS || status.isGranted || status.isLimited) { if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) { status = await Permission.camera.request(); if (!status.isGranted && !status.isLimited) { _showPermissionDialog("Camera"); return; } } } final XFile? image = await picker.pickImage( source: ImageSource.camera, maxWidth: 1920, maxHeight: 1080, imageQuality: 85, ); if (image != null) { setState(() => selectedFiles.add(File(image.path))); } } catch (e) { debugPrint("❌ Camera error: $e"); // If it's a simulator, it will throw an error since there is no camera if (Platform.isIOS && e.toString().contains("camera not available")) { _showErrorSnackbar("Camera not available on iOS Simulator"); } else { _showErrorSnackbar("Failed to capture image"); } } } Future pickFromGallery() async { Navigator.pop(context); try { var status = await Permission.photos.status; if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) { status = await Permission.photos.request(); if (!status.isGranted && !status.isLimited && !Platform.isIOS) { _showPermissionDialog("Photos"); return; } } final List? images = await picker.pickMultiImage( maxWidth: 1920, maxHeight: 1080, imageQuality: 85, ); if (images != null && images.isNotEmpty) { setState(() { selectedFiles.addAll(images.map((e) => File(e.path))); }); } } catch (e) { print("❌ Gallery error: $e"); _showErrorSnackbar("Failed to pick images"); } } Future pickDocuments() async { Navigator.pop(context); try { FilePickerResult? result = await FilePicker.platform.pickFiles( allowMultiple: true, type: FileType.any, ); if (result != null && result.files.isNotEmpty) { setState(() { selectedFiles.addAll( result.paths.where((e) => e != null).map((e) => File(e!)), ); }); } } catch (e) { print("❌ File picker error: $e"); _showErrorSnackbar("Failed to pick files"); } } void _showPermissionDialog(String permissionName) { showDialog( context: context, builder: (context) => AlertDialog( title: Text("$permissionName Permission Required"), content: Text( "Allow Taxglide to access your $permissionName to capture and upload files easily. You can enable this in the app settings.", ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("Cancel"), ), TextButton( onPressed: () { Navigator.pop(context); openAppSettings(); }, child: const Text("Go to Settings"), ), ], ), ); } void _showErrorSnackbar(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Colors.red, content: Text(message), duration: const Duration(seconds: 2), ), ); } void removeFile(int index) { setState(() { selectedFiles.removeAt(index); }); } String getFileName(String path) { return path.split('/').last; } IconData getFileIcon(String path) { final ext = path.split('.').last.toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) { return Icons.image; } else if (ext == 'pdf') { return Icons.picture_as_pdf; } else if (['doc', 'docx'].contains(ext)) { return Icons.description; } else if (['xls', 'xlsx'].contains(ext)) { return Icons.table_chart; } else if (['zip', 'rar', '7z'].contains(ext)) { return Icons.folder_zip; } else { return Icons.insert_drive_file; } } Future sendMessage() async { if (_isSending) return; // Prevent double-tap String text = messageCtrl.text.trim(); if (text.isEmpty && selectedFiles.isEmpty) return; // ⭐ Get the tagged message final taggedMessage = ref.read(taggedMessageProvider); final tagId = taggedMessage?.id ?? 0; setState(() => _isSending = true); // ⭐ OPTIMISTIC UI: Create a temporary message and add it immediately final tempId = DateTime.now().millisecondsSinceEpoch; final tempMsg = MessageModel( id: tempId, chatBy: "user", // User message: text, type: selectedFiles.isNotEmpty ? "file" : "text", isDelivered: 0, isRead: 0, createdAt: DateTime.now().toIso8601String(), userId: 0, tagId: tagId > 0 ? tagId : null, parentTag: taggedMessage != null ? ParentTagModel( id: taggedMessage.id, message: taggedMessage.message, type: taggedMessage.type, fileName: taggedMessage.fileName, ) : null, uploadedDocuments: selectedFiles.map((file) => DocumentModel( filePath: file.path, fileName: file.path.split('/').last, fileType: "image", )).toList(), documents: [], ); // Add to UI immediately final notifier = ref.read(chatMessagesProvider(widget.chatId).notifier); notifier.addNewMessage(tempMsg); // Clear input immediately for snappy feel final savedSelectedFiles = List.from(selectedFiles); messageCtrl.clear(); selectedFiles.clear(); ref.read(taggedMessageProvider.notifier).state = null; // Notify parent to scroll to bottom instantly if (widget.onMessageSent != null) { widget.onMessageSent!(); } try { List filesToUpload = []; // ⭐⭐⭐ Save files to Send folder if (savedSelectedFiles.isNotEmpty) { for (File file in savedSelectedFiles) { try { // Try to save to Send folder, fallback to original if fails final savedPath = await DownloadHelper.saveToSendFolder(file); filesToUpload.add(File(savedPath)); } catch (e) { filesToUpload.add(file); } } } // Send message with files await ApiRepository().sendChatMessage( chatId: widget.chatId, messages: text, tagId: tagId, files: filesToUpload, ); // Refresh chat messages (this will replace the temp message with the real one) await notifier.refresh(); ref.invalidate(chatDocumentProvider); if (mounted) { setState(() => _isSending = false); } } catch (e) { print("❌ Error sending message: $e"); if (mounted) { setState(() => _isSending = false); } // If it failed, delete the temporary message notifier.deleteMessage(tempId); // Show error message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Colors.red, content: Row( children: [ const Icon(Icons.error_outline, color: Colors.white), const SizedBox(width: 8), Expanded( child: Text( "Failed to send: ${e.toString()}", maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), duration: const Duration(seconds: 4), action: SnackBarAction( label: "Retry", textColor: Colors.white, onPressed: sendMessage, ), ), ); } } } @override Widget build(BuildContext context) { final taggedMessage = ref.watch(taggedMessageProvider); return SafeArea( bottom: true, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), color: Colors.white, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ⭐⭐⭐ SELECTED FILES PREVIEW (like WhatsApp) if (selectedFiles.isNotEmpty) Container( height: 100, margin: const EdgeInsets.only(bottom: 10), child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: selectedFiles.length, itemBuilder: (context, index) { final file = selectedFiles[index]; final isImage = DownloadHelper.isImageFile(file.path); return Container( width: 80, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(8), ), child: Stack( children: [ // File preview ClipRRect( borderRadius: BorderRadius.circular(8), child: isImage ? Image.file( file, width: 80, height: 100, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Center( child: Icon(Icons.broken_image), ); }, ) : Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( getFileIcon(file.path), size: 30, color: Colors.grey[600], ), const SizedBox(height: 4), Padding( padding: const EdgeInsets.symmetric( horizontal: 4, ), child: Text( getFileName(file.path), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle(fontSize: 9), ), ), ], ), ), ), // Remove button Positioned( top: 2, right: 2, child: GestureDetector( onTap: () => removeFile(index), child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), child: const Icon( Icons.close, size: 16, color: Colors.white, ), ), ), ), ], ), ); }, ), ), // Input Row Row( children: [ Expanded( child: Container( constraints: const BoxConstraints( minHeight: 62, maxHeight: 150, ), padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(7), border: Border.all(color: const Color(0xFFCACACA)), boxShadow: const [ BoxShadow( color: Colors.black26, offset: Offset(0, 4), blurRadius: 7.8, ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: messageCtrl, maxLines: null, minLines: 1, keyboardType: TextInputType.multiline, textInputAction: TextInputAction.newline, decoration: InputDecoration( hintText: taggedMessage != null ? "Reply to ${taggedMessage.user?.name ?? 'message'}" : "Send your message", border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( vertical: 12, ), ), ), ), const SizedBox(width: 8), GestureDetector( onTap: _isSending ? null : showPickerMenu, child: Padding( padding: const EdgeInsets.only(bottom: 12), child: Icon( Icons.attach_file, size: 26, color: _isSending ? Colors.grey : Colors.black, ), ), ), ], ), ), ), const SizedBox(width: 10), GestureDetector( onTap: _isSending ? null : sendMessage, child: Container( height: 46, width: 46, decoration: BoxDecoration( color: _isSending ? Colors.grey : const Color(0xFF08710C), shape: BoxShape.circle, ), child: Center( child: _isSending ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : const Icon( Icons.send, color: Colors.white, size: 22, ), ), ), ), ], ), ], ), ), ); } @override void dispose() { messageCtrl.dispose(); super.dispose(); } }