import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import '../controllers/chat_controller.dart'; import '../models/conversation_model.dart'; import '../models/message_model.dart'; import '../theme/app_theme.dart'; import '../widgets/message_bubble.dart'; class ChatScreen extends StatelessWidget { final ConversationModel conversation; const ChatScreen({super.key, required this.conversation}); @override Widget build(BuildContext context) { final ctrl = Get.put( ChatController(conversation: conversation), tag: conversation.id, ); final isDark = AppTheme.isDark(context); return Scaffold( backgroundColor: AppTheme.chatBackground(context), appBar: _buildAppBar(context, conversation, ctrl), body: Column( children: [ Expanded(child: _buildMessageList(context, ctrl)), _buildInputBar(context, ctrl), ], ), ); } PreferredSizeWidget _buildAppBar( BuildContext context, ConversationModel chat, ChatController ctrl, ) { final isDark = AppTheme.isDark(context); return AppBar( backgroundColor: AppTheme.surface(context), leadingWidth: 36, titleSpacing: 0, title: InkWell( onTap: () {}, // Future: open contact info child: Row( children: [ _buildAppBarAvatar(context, chat), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( chat.name, style: TextStyle( color: isDark ? AppTheme.darkTextPrimary : Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), // Status line _buildStatusLine(context, chat, ctrl), ], ), ), ], ), ), actions: [ IconButton( icon: Icon( Icons.videocam_outlined, color: isDark ? AppTheme.darkTextSecondary : Colors.white, ), onPressed: null, ), IconButton( icon: Icon( Icons.call_outlined, color: isDark ? AppTheme.darkTextSecondary : Colors.white, ), onPressed: null, ), IconButton( icon: Icon( Icons.more_vert, color: isDark ? AppTheme.darkTextSecondary : Colors.white, ), onPressed: null, ), ], ); } Widget _buildStatusLine( BuildContext context, ConversationModel chat, ChatController ctrl, ) { final isDark = AppTheme.isDark(context); final color = isDark ? AppTheme.darkTextSecondary : Colors.white.withOpacity(0.85); if (chat.isGroup) { return Obx(() { final count = ctrl.messages.length; return Text( count > 0 ? 'tap for group info' : 'Group', style: TextStyle(color: color, fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis, ); }); } // For 1:1 chats, show "online" placeholder or nothing // (Real status would come from the bridge server) return Text( '', style: TextStyle(color: color, fontSize: 12), ); } Widget _buildAppBarAvatar(BuildContext context, ConversationModel chat) { final isDark = AppTheme.isDark(context); final fallbackBg = isDark ? const Color(0xff2a3942) : Colors.white.withOpacity(0.25); if (chat.avatar != null && chat.avatar!.isNotEmpty) { return CircleAvatar( radius: 18, backgroundColor: fallbackBg, child: ClipOval( child: CachedNetworkImage( imageUrl: chat.avatar!, width: 36, height: 36, fit: BoxFit.cover, placeholder: (_, __) => _defaultAvatarIcon(chat, fallbackBg), errorWidget: (_, __, ___) => _defaultAvatarIcon(chat, fallbackBg), ), ), ); } return CircleAvatar( radius: 18, backgroundColor: fallbackBg, child: _defaultAvatarIcon(chat, fallbackBg), ); } Widget _defaultAvatarIcon(ConversationModel chat, Color bg) { return Icon( chat.isGroup ? Icons.group : Icons.person, color: Colors.white, size: 20, ); } Widget _buildMessageList(BuildContext context, ChatController ctrl) { return Obx(() { if (ctrl.isLoading.value && ctrl.messages.isEmpty) { return Center( child: CircularProgressIndicator(color: AppTheme.primary), ); } final items = ctrl.groupedMessages; if (items.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.chat_bubble_outline, color: AppTheme.textSecondary(context).withOpacity(0.4), size: 48, ), const SizedBox(height: 12), Text( 'No messages yet', style: TextStyle( color: AppTheme.textSecondary(context).withOpacity(0.8)), ), ], ), ); } return ListView.builder( controller: ctrl.scrollCtrl, padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), itemCount: items.length, itemBuilder: (_, i) { final item = items[i]; if (item is String) return _buildDateSeparator(context, item); return MessageBubble(message: item as MessageModel); }, ); }); } Widget _buildDateSeparator(BuildContext context, String label) { final isDark = AppTheme.isDark(context); return Center( child: Container( margin: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( color: isDark ? const Color(0xff1d2b33) : const Color(0xffd1f4cc), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Text( label, style: TextStyle( color: isDark ? AppTheme.darkTextSecondary : const Color(0xff54656f), fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ); } Widget _buildInputBar(BuildContext context, ChatController ctrl) { final isDark = AppTheme.isDark(context); final barBg = isDark ? AppTheme.darkBackground : AppTheme.lightBackground; final inputBg = isDark ? AppTheme.darkSurfaceLight : AppTheme.lightSurfaceLight; return Container( color: barBg, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: SafeArea( child: Obx(() { // ── Recording UI ───────────────────────────────────────────────── if (ctrl.isRecording.value) { return Row( children: [ const SizedBox(width: 12), const Icon(Icons.fiber_manual_record, color: Colors.red, size: 14), const SizedBox(width: 6), const Text( 'Recording...', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: 14), ), const SizedBox(width: 12), Text( '${(ctrl.recordDuration.value ~/ 60).toString().padLeft(2, '0')}:${(ctrl.recordDuration.value % 60).toString().padLeft(2, '0')}', style: TextStyle( color: AppTheme.textPrimary(context), fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]), ), const Spacer(), TextButton.icon( icon: const Icon(Icons.delete, color: Colors.redAccent, size: 18), label: const Text('Cancel', style: TextStyle(color: Colors.redAccent)), onPressed: ctrl.cancelRecording, ), const SizedBox(width: 8), GestureDetector( onTap: ctrl.stopAndSendRecording, child: Container( width: 44, height: 44, decoration: const BoxDecoration( color: AppTheme.primary, shape: BoxShape.circle, ), child: const Icon(Icons.check, color: Colors.white, size: 20), ), ), const SizedBox(width: 8), ], ); } // ── Normal Input ───────────────────────────────────────────────── return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ // ── Text input field ───────────────────────────────────────── Expanded( child: Container( decoration: BoxDecoration( color: inputBg, borderRadius: BorderRadius.circular(24), ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ // Emoji button IconButton( icon: Icon(Icons.emoji_emotions_outlined, color: AppTheme.textSecondary(context)), onPressed: null, padding: const EdgeInsets.only(bottom: 2), ), Expanded( child: TextField( controller: ctrl.inputCtrl, style: TextStyle( color: AppTheme.textPrimary(context), fontSize: 15), maxLines: 5, minLines: 1, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: 'Message', hintStyle: TextStyle( color: AppTheme.textSecondary(context)), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( vertical: 10, ), ), ), ), // Attachment button IconButton( icon: Icon(Icons.attach_file, color: AppTheme.textSecondary(context)), onPressed: () => _showAttachmentSheet(context, ctrl), padding: const EdgeInsets.only(bottom: 2), ), // Camera button (only when no text) Obx(() => ctrl.hasText.value ? const SizedBox.shrink() : IconButton( icon: Icon(Icons.camera_alt_outlined, color: AppTheme.textSecondary(context)), onPressed: () => _pickAndSendImage(ctrl, ImageSource.camera), padding: const EdgeInsets.only(bottom: 2, right: 4), )), ], ), ), ), const SizedBox(width: 8), // ── Send / Mic button ───────────────────────────────────────── GestureDetector( onTap: () { if (ctrl.hasText.value) { ctrl.sendMessage(); } else { ctrl.startRecording(); } }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 48, height: 48, decoration: const BoxDecoration( color: AppTheme.primary, shape: BoxShape.circle, ), child: ctrl.isSending.value ? const Padding( padding: EdgeInsets.all(13), child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Obx(() => Icon( ctrl.hasText.value ? Icons.send_rounded : Icons.mic_rounded, color: Colors.white, size: 22, )), ), ), ], ); }), ), ); } void _showAttachmentSheet(BuildContext context, ChatController ctrl) { final isDark = AppTheme.isDark(context); Get.bottomSheet( Container( decoration: BoxDecoration( color: isDark ? AppTheme.darkSurface : Colors.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 4, decoration: BoxDecoration( color: AppTheme.textSecondary(context).withOpacity(0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildAttachmentItem( context: context, icon: Icons.photo_library_rounded, color: const Color(0xff7c4dff), label: 'Gallery', onTap: () { Get.back(); _pickAndSendImage(ctrl, ImageSource.gallery); }, ), _buildAttachmentItem( context: context, icon: Icons.camera_alt_rounded, color: const Color(0xffff4081), label: 'Camera', onTap: () { Get.back(); _pickAndSendImage(ctrl, ImageSource.camera); }, ), _buildAttachmentItem( context: context, icon: Icons.insert_drive_file_rounded, color: const Color(0xff2196f3), label: 'Document', onTap: () => Get.back(), ), _buildAttachmentItem( context: context, icon: Icons.mic_rounded, color: const Color(0xffff9800), label: 'Audio', onTap: () { Get.back(); ctrl.startRecording(); }, ), ], ), ], ), ), barrierColor: Colors.black.withOpacity(0.5), ); } void _pickAndSendImage(ChatController ctrl, ImageSource source) async { try { final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( source: source, imageQuality: 75, ); if (image == null) return; final bytes = await image.readAsBytes(); final base64String = base64Encode(bytes); String mimetype = 'image/jpeg'; if (image.path.toLowerCase().endsWith('.png')) { mimetype = 'image/png'; } else if (image.path.toLowerCase().endsWith('.gif')) { mimetype = 'image/gif'; } await ctrl.sendMediaMessage( base64String, mimetype, image.name, ); } catch (e) { Get.snackbar( 'Error picking image', e.toString(), backgroundColor: Colors.redAccent.withOpacity(0.8), colorText: Colors.white, ); } } Widget _buildAttachmentItem({ required BuildContext context, required IconData icon, required Color color, required String label, required VoidCallback onTap, }) { final isDark = AppTheme.isDark(context); return GestureDetector( onTap: onTap, child: Column( children: [ Container( width: 54, height: 54, decoration: BoxDecoration( color: color.withOpacity(0.12), shape: BoxShape.circle, ), child: Icon(icon, color: color, size: 26), ), const SizedBox(height: 8), Text( label, style: TextStyle( color: AppTheme.textPrimary(context), fontSize: 12), ), ], ), ); } }