import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../core/chat/chat_bloc.dart'; import '../../../core/chat/session_closure_bloc.dart'; import '../widgets/pricing_bottom_sheet.dart'; class ChatScreen extends StatefulWidget { final String sessionId; final String mitraName; const ChatScreen({super.key, required this.sessionId, required this.mitraName}); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { final _messageController = TextEditingController(); final _scrollController = ScrollController(); Timer? _typingThrottle; @override void dispose() { _messageController.dispose(); _scrollController.dispose(); _typingThrottle?.cancel(); super.dispose(); } void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, ); } }); } void _onTextChanged(String text) { if (_typingThrottle?.isActive ?? false) return; context.read().add(SendTyping()); _typingThrottle = Timer(const Duration(seconds: 2), () {}); } void _sendMessage() { final text = _messageController.text.trim(); if (text.isEmpty) return; context.read().add(SendMessage(text)); _messageController.clear(); _scrollToBottom(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (prev, curr) { if (prev is ChatConnected && curr is ChatConnected) { return prev.sessionExpired != curr.sessionExpired || prev.sessionClosing != curr.sessionClosing || prev.messages.length != curr.messages.length; } return true; }, listener: (context, state) { if (state is ChatConnected) { if (state.sessionClosing) { context.read().add(DeclineExtension()); } _scrollToBottom(); // Auto-mark received messages as read final unread = state.messages .where((m) => m.senderType == 'mitra' && m.status != 'read') .map((m) => m.id) .toList(); if (unread.isNotEmpty) { context.read().add(MarkMessagesRead(unread)); } } }, ), BlocListener( listener: (context, state) { if (state is ClosureComplete) { context.go('/home'); } }, ), ], child: Scaffold( appBar: AppBar( title: Text(widget.mitraName), automaticallyImplyLeading: false, actions: [ BlocBuilder( builder: (context, state) { if (state is ChatConnected && state.remainingSeconds != null) { return Padding( padding: const EdgeInsets.only(right: 16), child: Center( child: Text( '${state.remainingSeconds}s', style: TextStyle( color: state.remainingSeconds! < 30 ? Colors.red : null, fontWeight: FontWeight.bold, ), ), ), ); } return const SizedBox.shrink(); }, ), ], ), body: BlocBuilder( builder: (context, state) { if (state is ChatConnecting) { return const Center(child: CircularProgressIndicator()); } if (state is ChatError) { return Center(child: Text(state.message)); } if (state is ChatConnected) { return _buildChatBody(context, state); } return const SizedBox.shrink(); }, ), ), ); } Widget _buildChatBody(BuildContext context, ChatConnected state) { // Show session expired dialog if (state.sessionExpired) { return _buildExpiredView(context); } // Show goodbye input final closureState = context.watch().state; if (closureState is ClosureShowGoodbye || closureState is ClosureSubmitting) { return _buildGoodbyeView(context, closureState); } if (state.sessionPaused) { return _buildPausedView(); } return Column( children: [ Expanded( child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: state.messages.length, itemBuilder: (context, index) { final msg = state.messages[index]; final isMe = msg.senderType == 'customer'; return _buildMessageBubble(msg, isMe); }, ), ), if (state.isOtherTyping) const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Align( alignment: Alignment.centerLeft, child: Text('Bestie sedang mengetik...', style: TextStyle(color: Colors.grey, fontSize: 12)), ), ), _buildInputBar(context, state), ], ); } Widget _buildMessageBubble(ChatMessage msg, bool isMe) { return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.75), decoration: BoxDecoration( color: isMe ? Colors.blue.shade100 : Colors.grey.shade200, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(msg.content, style: const TextStyle(fontSize: 15)), const SizedBox(height: 4), Row( mainAxisSize: MainAxisSize.min, children: [ Text( '${msg.createdAt.hour.toString().padLeft(2, '0')}:${msg.createdAt.minute.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 10, color: Colors.grey), ), if (isMe) ...[ const SizedBox(width: 4), _buildStatusIcon(msg.status), ], ], ), ], ), ), ); } Widget _buildStatusIcon(String status) { switch (status) { case 'sending': return const Icon(Icons.access_time, size: 14, color: Colors.grey); case 'sent': return const Icon(Icons.check, size: 14, color: Colors.grey); case 'delivered': return const Icon(Icons.done_all, size: 14, color: Colors.grey); case 'read': return const Icon(Icons.done_all, size: 14, color: Colors.blue); default: return const SizedBox.shrink(); } } Widget _buildInputBar(BuildContext context, ChatConnected state) { return SafeArea( child: Padding( padding: const EdgeInsets.all(8), child: Row( children: [ Expanded( child: TextField( controller: _messageController, onChanged: _onTextChanged, textInputAction: TextInputAction.send, onSubmitted: (_) => _sendMessage(), decoration: InputDecoration( hintText: 'Ketik pesan...', border: OutlineInputBorder(borderRadius: BorderRadius.circular(24)), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.send, color: Colors.blue), onPressed: _sendMessage, ), ], ), ), ); } Widget _buildExpiredView(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.timer_off, size: 64, color: Colors.orange), const SizedBox(height: 16), const Text('Waktu sesi habis', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Text('Apakah kamu ingin memperpanjang sesi?', textAlign: TextAlign.center), const SizedBox(height: 24), ElevatedButton( onPressed: () => PricingBottomSheet.show(context), child: const Text('Perpanjang Sesi'), ), const SizedBox(height: 12), TextButton( onPressed: () => context.read().add(DeclineExtension()), child: const Text('Tidak, akhiri sesi'), ), ], ), ), ); } Widget _buildGoodbyeView(BuildContext context, SessionClosureState closureState) { final controller = TextEditingController(); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.waving_hand, size: 64, color: Colors.amber), const SizedBox(height: 16), const Text('Pesan Penutup', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Text('Tuliskan pesan terakhirmu untuk Bestie', textAlign: TextAlign.center), const SizedBox(height: 24), TextField( controller: controller, maxLines: 3, decoration: InputDecoration( hintText: 'Terima kasih, Bestie...', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 16), ElevatedButton( onPressed: closureState is ClosureSubmitting ? null : () { final text = controller.text.trim(); if (text.isNotEmpty) { context.read().add( SubmitGoodbye(sessionId: widget.sessionId, message: text), ); } }, child: closureState is ClosureSubmitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Kirim & Selesai'), ), ], ), ), ); } Widget _buildPausedView() { return const Center( child: Padding( padding: EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 24), Text('Menunggu konfirmasi Bestie...', style: TextStyle(fontSize: 18)), SizedBox(height: 8), Text('Chat dijeda sementara', style: TextStyle(color: Colors.grey)), ], ), ), ); } }