Files
kent_logistics_app/lib/screens/chat_screen.dart
2025-12-16 10:24:16 +05:30

297 lines
7.3 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _messageCtrl = TextEditingController();
final ScrollController _scrollCtrl = ScrollController();
late ChatService _chatService;
final ReverbSocketService _socket = ReverbSocketService();
int? ticketId;
List<Map<String, dynamic>> 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<ChatUnreadProvider>().setChatOpen(true);
});
_initChat();
}
Future<void> _pickAndSendFile() async {
final XFile? file = await openFile();
if (file == null || ticketId == null) return;
final dartFile = File(file.path);
await _chatService.sendFile(ticketId!, dartFile);
}
// ============================
// INIT CHAT
// ============================
Future<void> _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<Map<String, dynamic>>.from(msgs);
// 3⃣ Realtime socket
await _socket.connect(
context: context,
ticketId: ticketId!,
onMessage: (msg) {
if (!mounted) return;
setState(() => messages.add(msg));
_scrollToBottom();
},
onAdminMessage: () {
if (!mounted) {
context.read<ChatUnreadProvider>().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<void> _sendMessage() async {
final text = _messageCtrl.text.trim();
if (text.isEmpty) return;
_messageCtrl.clear();
await _chatService.sendMessage(
ticketId!,
message: text,
);
}
// ============================
// DISPOSE
// ============================
@override
void dispose() {
// 🔕 Mark chat CLOSED
context.read<ChatUnreadProvider>().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<void> _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.builder(
controller: _scrollCtrl,
padding: const EdgeInsets.all(12),
itemCount: messages.length,
itemBuilder: (_, index) {
final msg = messages[index];
final isUser = msg['sender_type'] == 'App\\Models\\User';
final String? filePath = msg['file_path'];
final String? fileType = msg['file_type'];
final String? message = msg['message'];
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),
),
// 🔽 ONLY THIS PART CHANGED
child: filePath == null
? Text(
message ?? '',
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
),
)
: ChatFilePreview(
filePath: filePath,
fileType: fileType ?? '',
isUser: isUser,
),
),
);
},
);
}
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,
),
],
),
);
}
}