import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:onufitness/services/logger_service.dart'; import 'package:intl/intl.dart'; import 'package:agora_chat_sdk/agora_chat_sdk.dart'; import 'package:onufitness/constants/api_enum_constant.dart'; import 'package:onufitness/constants/asset_constants.dart'; import 'package:onufitness/constants/color_constant.dart'; import 'package:onufitness/constants/text_constant.dart'; import 'package:onufitness/screens/chat/controllers/chat_controller.dart'; import 'package:onufitness/screens/chat/widgets/chat_input_area.dart'; import 'package:onufitness/screens/chat/widgets/voice/voice_message_buble_widget.dart'; import 'package:onufitness/screens/echoboard/widget/image_zoom.dart'; import 'package:onufitness/screens/chat/widgets/chat_appbar.dart'; import 'package:onufitness/screens/chat/widgets/chat_video_widgets.dart'; import 'package:onufitness/services/agora/agora_chat_service.dart'; import 'package:onufitness/utils/custom_sneakbar.dart'; class ChatDetailScreen extends StatefulWidget { final String targetUserId; final String targetUserName; final String? targetUserAvatar; final bool isGroupMessage; const ChatDetailScreen({ super.key, required this.targetUserId, required this.targetUserName, this.targetUserAvatar, this.isGroupMessage = false, }); @override State createState() => _ChatDetailScreenState(); } class _ChatDetailScreenState extends State { late ChatController chatCtrl; late TextEditingController textController; late ScrollController scrollController; AgoraChatService agoraService = Get.put(AgoraChatService()); final logger = LoggerService(); // Track if we should maintain scroll position during pagination bool maintainScrollPosition = false; double scrollPosition = 0.0; @override void initState() { if (!Get.isRegistered()) { Get.put(ChatController()); } chatCtrl = Get.find(); super.initState(); textController = TextEditingController(); scrollController = ScrollController(); chatCtrl.isGroupChattt.value = widget.isGroupMessage; // Load existing messages for this conversation WidgetsBinding.instance.addPostFrameCallback((_) { // chatCtrl.isGroupChattt.value = widget.isGroupMessage; initAgoraChatService(); isMyFriendCheck(); loadMessages(); // Set up listeners setupControllers(); subscribeToUserPresence(); }); } @override void dispose() { textController.dispose(); scrollController.dispose(); super.dispose(); } initAgoraChatService() async { if (agoraService.currentUserId == null) { await Future.delayed(Duration(milliseconds: 500)); // Check again if (agoraService.currentUserId == null) { bool success = await agoraService.loginToAgora(); if (!success) { customSnackbar( title: "Error", message: "Failed to connect to chat", duration: 2, ); Get.back(); return; } } } } subscribeToUserPresence() async { await chatCtrl.subscribeToUserPresence(widget.targetUserId); } isMyFriendCheck() async { await chatCtrl.friendCheck(connectedAgoraUserID: widget.targetUserId); } Future loadMessages() async { // Load messages from the conversation await chatCtrl.loadMessages(widget.targetUserId); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); }); } void setupControllers() { // For tracking scroll direction scrollController.addListener(() { final position = scrollController.position; // Detect upward scroll near top to trigger pagination if (position.userScrollDirection == ScrollDirection.forward && position.pixels <= 100) { _loadMoreMessages(); } }); // ONLY scroll to bottom when a new message is added at the end ever(chatCtrl.messagesMap, (map) { final messages = map[widget.targetUserId] ?? []; // Only scroll to bottom if this is a NEW message appended if (messages.isNotEmpty) { final lastMsg = messages.last; final isNewMessage = DateTime.fromMillisecondsSinceEpoch( lastMsg.serverTime, ).difference(DateTime.now()).inSeconds.abs() < 5; if (isNewMessage) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); }); } } }); } Future _loadMoreMessages() async { final hasMore = chatCtrl.hasMoreMessages[widget.targetUserId] ?? false; final isLoading = chatCtrl.isLoadingMore[widget.targetUserId] ?? false; if (hasMore && !isLoading) { // Save current scroll position scrollPosition = scrollController.position.pixels; // maintainScrollPosition = true; await chatCtrl.loadMoreMessages(widget.targetUserId); } } void _scrollToBottom() { if (scrollController.hasClients) { Future.delayed(const Duration(milliseconds: 100), () { if (scrollController.hasClients && scrollController.position.maxScrollExtent > 0) { scrollController.animateTo( scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (!didPop) { Get.back(); await chatCtrl.markAllMessagesAsRead(widget.targetUserId); chatCtrl.selectedUserId.value = ""; await chatCtrl.unsuscribePresence(widget.targetUserId); } }, child: Scaffold( resizeToAvoidBottomInset: true, body: Container( decoration: BoxDecoration( color: Color(primaryColor), image: DecorationImage( image: AssetImage(AssetConstants.chatAppBarBackgroundImage), fit: BoxFit.cover, opacity: 0.5, ), ), child: SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.max, // For Samsung phones children: [ // Reactive app bar Obx(() { final status = chatCtrl.presenceMap[widget.targetUserId] ?? ''; final isOnline = status == 'online'; return chatAppBar( context: context, chatCtrl: chatCtrl, receiverName: widget.targetUserName, showOnlineDot: isOnline, targetUserAvatarUrl: widget.targetUserAvatar, userId: widget.targetUserId, isGroupChat: widget.isGroupMessage, ); }), // Chat messages Expanded( child: ChatMessageList( chatCtrl: chatCtrl, scrollController: scrollController, targetUserId: widget.targetUserId, isGroupMessage: widget.isGroupMessage, ), ), // File upload loader FileUploadLoader( targetUserId: widget.targetUserId, chatCtrl: chatCtrl, ), // Conditionally show chat input if (widget.isGroupMessage == true) Column( children: [ ChatInputAreaWithVoice( chatCtrl: chatCtrl, textController: textController, targetUserId: widget.targetUserId, isGroupMessage: widget.isGroupMessage, targetUserName: widget.targetUserName, ), Container(color: Colors.white, height: 20.h), ], ) else Obx(() { if (chatCtrl.isMyFriendCheckLoading.value) { // Using this Insted of Loader ( for better UX) return Column( children: [ ChatInputAreaWithVoice( chatCtrl: chatCtrl, textController: textController, targetUserId: widget.targetUserId, isGroupMessage: widget.isGroupMessage, targetUserName: widget.targetUserName, ), ], ); } if (chatCtrl.isMyFriend.value) { return Column( children: [ ChatInputAreaWithVoice( chatCtrl: chatCtrl, textController: textController, targetUserId: widget.targetUserId, isGroupMessage: widget.isGroupMessage, targetUserName: widget.targetUserName, ), ], ); } return Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.only(bottom: 20.sp, top: 15.sp), child: Text( "You cannot send messages to this user right now.", textAlign: TextAlign.center, style: TextStyle(color: Colors.black), ), ), Container( height: MediaQuery.of(context).padding.bottom, ), ], ); }), ], ), ), ), ), ); } } // Message List Component with Pagination Support class ChatMessageList extends StatelessWidget { final ChatController chatCtrl; final ScrollController scrollController; final String targetUserId; final bool isGroupMessage; const ChatMessageList({ super.key, required this.chatCtrl, required this.scrollController, required this.targetUserId, this.isGroupMessage = false, }); @override Widget build(BuildContext context) { return Container( color: Colors.white, child: Column( children: [ // End-to-End Encryption Notice const EncryptionNotice(), // Message List Expanded(child: Obx(() => messageListContent())), ], ), ); } Widget messageListContent() { final messages = chatCtrl.messagesMap[targetUserId] ?? []; final isLoadingMore = chatCtrl.isLoadingMore[targetUserId] ?? false; final hasMoreMessages = chatCtrl.hasMoreMessages[targetUserId] ?? false; if (messages.isEmpty && !isLoadingMore) { return const EmptyChat(); } return ListView.builder( controller: scrollController, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), itemCount: messages.length + (isLoadingMore ? 1 : 0), itemBuilder: (_, i) { // Show loading indicator at the top if (i == 0 && isLoadingMore) { return const LoadingIndicator(); } final messageIndex = isLoadingMore ? i - 1 : i; final msg = messages[messageIndex]; final showDate = messageIndex == 0 || !_isSameDay( DateTime.fromMillisecondsSinceEpoch( messages[messageIndex - 1].serverTime, ), DateTime.fromMillisecondsSinceEpoch(msg.serverTime), ); final isGroupMsg = isGroupMessage; final isMe = msg.from == ChatClient.getInstance.currentUserId; // Get sender name for group messages String? senderName; if (isGroupMsg && !isMe) { // First try to get from userInfoMap final userInfo = chatCtrl.userInfoMap[msg.from]; if (userInfo != null && userInfo.nickName?.isNotEmpty == true) { senderName = userInfo.nickName; } else if (userInfo != null && userInfo.userId.isNotEmpty == true) { senderName = userInfo.userId; } else { // Fallback to sender's user ID if no display name is available // senderName = msg.from ?? 'Unknown'; senderName = 'Unknown'; // Trigger fetch of user info for future messages if (msg.from != null) { chatCtrl.fetchUserInfo(msg.from!); } } } return Column( children: [ if (showDate) _DateSeparator( date: DateTime.fromMillisecondsSinceEpoch(msg.serverTime), ), CustomMessageBubble( message: msg, isMe: isMe, isGroupMessage: isGroupMsg, senderName: senderName, chatController: chatCtrl, ), // Show "Load All Messages" button at the top (after loading indicator) if (messageIndex == 0 && !isLoadingMore && hasMoreMessages) LoadAllMessagesButton( chatCtrl: chatCtrl, targetUserId: targetUserId, ), ], ); }, ); } bool _isSameDay(DateTime date1, DateTime date2) { return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day; } } // End-to-End Encryption Notice Widget class EncryptionNotice extends StatelessWidget { const EncryptionNotice({super.key}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration(color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.lock, size: regularSizeText, color: lightGreyColor), const SizedBox(width: 8), Text( "End to End Encrypted", style: TextStyle( fontSize: smallSizeText, color: lightGreyColor, fontWeight: FontWeight.w500, ), ), ], ), ); } } // Loading Indicator Widget class LoadingIndicator extends StatelessWidget { const LoadingIndicator({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(height: 8), Text( "Loading messages...", style: TextStyle(color: Colors.grey, fontSize: 12), ), ], ), ), ); } } // Load All Messages Button Widget class LoadAllMessagesButton extends StatelessWidget { final ChatController chatCtrl; final String targetUserId; const LoadAllMessagesButton({ super.key, required this.chatCtrl, required this.targetUserId, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.all(16), child: Center( child: ElevatedButton.icon( onPressed: () async { await chatCtrl.loadAllMessages(targetUserId); }, icon: const Icon(Icons.history, size: 16), label: const Text("Load All Messages"), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade50, foregroundColor: Colors.blue.shade700, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), ); } } class EmptyChat extends StatelessWidget { const EmptyChat({super.key}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.chat_bubble_outline, size: 80, color: Colors.grey.withValues(alpha: 0.5), ), const SizedBox(height: 16), const Text( "No messages yet", style: TextStyle(color: Colors.grey, fontSize: 16), ), const SizedBox(height: 8), Text( "Say hello to start the conversation!", style: TextStyle(color: Colors.grey.shade600, fontSize: 14), ), ], ), ); } } class _DateSeparator extends StatelessWidget { final DateTime date; const _DateSeparator({required this.date}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ Expanded(child: Divider(color: Colors.grey.shade300)), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12), ), child: Text( _formatMessageDate(date), style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), ), ), Expanded(child: Divider(color: Colors.grey.shade300)), ], ), ); } String _formatMessageDate(DateTime date) { final now = DateTime.now(); if (_isSameDay(date, now)) { return "Today"; } else if (_isSameDay(date, now.subtract(const Duration(days: 1)))) { return "Yesterday"; } else if (now.difference(date).inDays < 7) { return DateFormat('EEEE').format(date); } else { return DateFormat('MMM d, yyyy').format(date); } } bool _isSameDay(DateTime date1, DateTime date2) { return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day; } } class CustomMessageBubble extends StatelessWidget { final ChatMessage message; final bool isMe; final bool isGroupMessage; final String? senderName; final ChatController chatController; const CustomMessageBubble({ super.key, required this.message, required this.isMe, required this.chatController, this.isGroupMessage = false, this.senderName, }); @override Widget build(BuildContext context) { final timeString = DateFormat( 'h:mm a', ).format(DateTime.fromMillisecondsSinceEpoch(message.serverTime)); return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Container( constraints: BoxConstraints(maxWidth: Get.width * 0.75), margin: EdgeInsets.only( top: 4.h, bottom: 6.h, left: isMe ? 30.w : 15.w, right: isMe ? 15.w : 30.w, ), child: Column( crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( decoration: BoxDecoration( color: isMe ? Color(chatBubbleColor) : Colors.grey.shade200, borderRadius: BorderRadius.only( topLeft: Radius.circular(isMe ? 20 : 2), topRight: Radius.circular(isMe ? 2 : 20), bottomLeft: const Radius.circular(20), bottomRight: const Radius.circular(20), ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 4, offset: const Offset(0, 2), ), ], border: isMe ? null : Border.all( color: Colors.grey.withValues(alpha: 0.1), width: 1, ), ), child: Padding( padding: EdgeInsets.symmetric( horizontal: getPaddingHorizontal(), vertical: getPaddingVertical(), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Sender name for group messages if (isGroupMessage && !isMe && senderName != null) Padding( padding: const EdgeInsets.only(bottom: 6), child: Text( senderName!, style: TextStyle( fontSize: verySmallSizeText, color: deeperShadeOfprimaryColor, fontWeight: FontWeight.w600, ), ), ), // Message content based on type messageContentWidget(chatController), const SizedBox(height: 6), // Time and status row Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Text( timeString, style: TextStyle( fontSize: verySmallSizeText, color: isMe ? Colors.grey : Color(darkGreyColor), fontWeight: FontWeight.w400, ), ), if (isMe && !isGroupMessage) ...[ const SizedBox(width: 4), messageStatusIconWidget(), ], ], ), ], ), ), ), ], ), ), ); } double getPaddingHorizontal() { switch (message.body.type) { case MessageType.IMAGE: return 8.w; case MessageType.FILE: return 10.w; default: return 15.w; } } double getPaddingVertical() { switch (message.body.type) { case MessageType.IMAGE: return 8.h; case MessageType.FILE: return 10.w; default: return 5.h; } } Widget messageContentWidget(ChatController chatController) { final messageType = message.body.type; // Handle text messages if (messageType == MessageType.TXT) { return textMessageWidget(); } // Handle voice messages if (messageType == MessageType.VOICE) { return VoiceMessageBubble( message: message, isMe: isMe, chatController: chatController, ); } // Handle file-based messages if (message.body is! ChatFileMessageBody) { return unsupportedMessageWidget(); } final body = message.body as ChatFileMessageBody; final fileName = body.displayName?.toLowerCase().trim() ?? ''; final extension = chatController.getFileExtension(fileName); if (chatController.isImageExtension(extension)) { return imageMessageWidget(); } else if (chatController.isVideoExtension(extension)) { return videoMessageWidget(); } else if (chatController.isDocumentExtension(extension)) { return fileMessageWidget(); } else { return unsupportedMessageWidget(); } } Widget textMessageWidget() { final textBody = message.body as ChatTextMessageBody; return Text( textBody.content, style: TextStyle( fontSize: 16, color: isMe ? Colors.black : Colors.black, height: 1.3, ), ); } Widget imageMessageWidget() { final imageBody = message.body as ChatFileMessageBody; final chatCtrl = Get.find(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( constraints: BoxConstraints( maxWidth: Get.width * 0.6, maxHeight: 300, minWidth: 150, minHeight: 150, ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Stack( children: [ customImageContainer(imageBody), Positioned( top: 8, right: 8, child: Obx(() { final isDownloading = chatCtrl.isDownloading[message.msgId] ?? false; final progress = chatCtrl.downloadProgress[message.msgId] ?? 0.0; return Container( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(30), ), child: isDownloading ? Container( padding: const EdgeInsets.all(8), child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( value: progress, strokeWidth: 2, color: Colors.white, ), ), ) : IconButton( onPressed: () => chatCtrl.downloadFile(message), icon: const Icon( Icons.download, color: Colors.white, size: 18, ), padding: const EdgeInsets.all(4), constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), ), ); }), ), ], ), ), ), ], ); } Widget customImageContainer(ChatFileMessageBody imageBody) { if (imageBody.localPath.isNotEmpty == true) { final localFile = File(imageBody.localPath); if (localFile.existsSync()) { return GestureDetector( onTap: () => Get.to( () => ImageZoomWidget( imageUrl: imageBody.remotePath!, heroTag: "Hey_hero_${DateTime.now().millisecondsSinceEpoch}", ), ), child: Image.file( localFile, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => defaultImagePlaceholder(imageBody), ), ); } } // Try to display remote image if (imageBody.remotePath?.isNotEmpty == true) { return GestureDetector( onTap: () => Get.to( () => ImageZoomWidget( imageUrl: imageBody.remotePath!, heroTag: "${DateTime.now().millisecondsSinceEpoch}", ), ), child: Image.network( imageBody.remotePath!, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, errorBuilder: (context, error, stackTrace) => defaultImagePlaceholder(imageBody), ), ); } return defaultImagePlaceholder(imageBody); } Widget defaultImagePlaceholder(ChatFileMessageBody imageBody) { return Container( width: double.infinity, height: 150, color: Colors.grey.shade300, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image, size: 40, color: Colors.grey.shade600), const SizedBox(height: 8), Text( imageBody.displayName ?? 'Image', style: TextStyle(color: Colors.grey.shade600), textAlign: TextAlign.center, ), const SizedBox(height: 8), ], ), ); } Widget videoMessageWidget() { final videoBody = message.body as ChatFileMessageBody; final chatCtrl = Get.find(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( constraints: BoxConstraints( maxWidth: Get.width * 0.6, maxHeight: 150, minWidth: 150, minHeight: 150, ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Stack( children: [ Container( width: double.infinity, height: 150.h, color: Colors.black, ), // Play button overlay Positioned( top: 0, right: 0, bottom: 0, left: 0, child: Container( color: Colors.black.withValues(alpha: 0.3), child: Center( child: GestureDetector( onTap: () => playVideo(videoBody), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), child: const Icon( Icons.play_arrow, color: Colors.white, size: 30, ), ), ), ), ), ), // Download overlay if needed Obx(() { final isDownloading = chatCtrl.isDownloading[message.msgId] ?? false; final progress = chatCtrl.downloadProgress[message.msgId] ?? 0.0; if (isDownloading) { return Container( color: Colors.black.withValues(alpha: 0.5), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( value: progress, color: Colors.white, ), const SizedBox(height: 8), Text( '${(progress * 100).toInt()}%', style: const TextStyle(color: Colors.white), ), ], ), ), ); } return const SizedBox.shrink(); }), ], ), ), ), const SizedBox(height: 4), Row( children: [ Expanded( child: Text( videoBody.displayName ?? 'Video', style: TextStyle( fontSize: verySmallSizeText, color: isMe ? Colors.black : Colors.black, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 10), // Download button beside video file name Obx(() { final isDownloading = chatCtrl.isDownloading[message.msgId] ?? false; return GestureDetector( onTap: isDownloading ? null : () => chatCtrl.downloadFile(message), child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 5, ), decoration: BoxDecoration( color: isDownloading ? Colors.grey : Colors.blue, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isDownloading ? Icons.downloading : Icons.download, size: regularSizeText, color: Colors.white, ), ], ), ), ); }), ], ), ], ); } void playVideo(ChatFileMessageBody videoBody) async { String? videoPath; // Check if video is available locally if (videoBody.localPath.isNotEmpty == true && File(videoBody.localPath).existsSync()) { videoPath = videoBody.localPath; } else if (videoBody.remotePath?.isNotEmpty == true) { videoPath = videoBody.remotePath!; } if (videoPath != null) { Get.to(() => VideoPlayerScreen(videoPath: videoPath!)); } else { // Show download dialog Get.dialog( AlertDialog( title: const Text('Video Not Available'), content: const Text('Please download the video first to play it.'), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { Get.back(); final chatCtrl = Get.find(); chatCtrl.downloadFile(message); }, child: const Text('Download'), ), ], ), ); } } // Updated fileMessageWidget method for PDF and other file handling Widget fileMessageWidget() { final fileBody = message.body as ChatFileMessageBody; final chatCtrl = Get.find(); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isMe ? Colors.white.withValues(alpha: 0.2) : Colors.grey.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: getFileIconColor(fileBody.displayName ?? ''), borderRadius: BorderRadius.circular(6), ), child: Icon( getFileIcon(fileBody.displayName ?? ''), color: Colors.white, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( fileBody.displayName ?? 'Unknown File', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: isMe ? Colors.black87 : Colors.black, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( _formatFileSize(fileBody.fileSize!), style: TextStyle( fontSize: verySmallSizeText, color: isMe ? Colors.black : Colors.black, ), ), ], ), ), // Download button only Obx(() { final isDownloading = chatCtrl.isDownloading[message.msgId] ?? false; final progress = chatCtrl.downloadProgress[message.msgId] ?? 0.0; if (isDownloading) { return SizedBox( width: 40, height: 40, child: Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: progress, strokeWidth: 2, backgroundColor: Colors.grey.shade300, ), Text( '${(progress * 100).toInt()}%', style: const TextStyle(fontSize: 8), ), ], ), ); } return GestureDetector( onTap: () => chatCtrl.downloadFile(message), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(6), ), child: Icon(Icons.download, color: Colors.white, size: 16), ), ); }), ], ), ], ), ); } // New VideoPlayerScreen widget Widget unsupportedMessageWidget() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade300), ), child: Row( children: [ Icon(Icons.warning, color: Colors.orange.shade700), const SizedBox(width: 8), Expanded( child: Text( 'Unsupported message type: ${message.body.type}', style: TextStyle(color: Colors.orange.shade700, fontSize: 12), ), ), ], ), ); } Widget messageStatusIconWidget() { if (message.hasReadAck) { return Icon(Icons.done_all, size: 14, color: Colors.blue); } else if (message.hasDeliverAck) { return Icon(Icons.done_all, size: 14, color: Colors.grey); } else { return Icon(Icons.check, size: 14, color: Colors.grey); } } IconData getFileIcon(String fileName) { final extension = fileName.split('.').last.toLowerCase(); switch (extension) { case 'pdf': return Icons.picture_as_pdf; case 'doc': case 'docx': return Icons.description; case 'xls': case 'xlsx': return Icons.table_chart; case 'ppt': case 'pptx': return Icons.slideshow; case 'zip': case 'rar': return Icons.archive; case 'txt': return Icons.text_snippet; default: return Icons.insert_drive_file; } } Color getFileIconColor(String fileName) { final extension = fileName.split('.').last.toLowerCase(); switch (extension) { case 'pdf': return Colors.red; case 'doc': case 'docx': return Colors.blue; case 'xls': case 'xlsx': return Colors.green; case 'ppt': case 'pptx': return Colors.orange; case 'zip': case 'rar': return Colors.purple; case 'txt': return Colors.grey; default: return Colors.blueGrey; } } String _formatFileSize(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; if (bytes < 1024 * 1024 * 1024) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; } } class SendButton extends StatelessWidget { final ChatController chatCtrl; final TextEditingController textController; final String targetUserId; final bool isGroupMessage; final String targetUserName; const SendButton({ super.key, required this.chatCtrl, required this.textController, required this.targetUserId, this.isGroupMessage = false, required this.targetUserName, }); @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: textController, builder: (context, value, child) { return GestureDetector( onTap: () { _sendMessage(); }, onLongPress: () {}, child: Container( width: 45.w, height: 45.h, decoration: BoxDecoration( color: const Color(primaryColor), shape: BoxShape.circle, ), child: Icon(Icons.send, color: Colors.black), ), ); }, ); } void _sendMessage() { final text = textController.text.trim(); if (text.isNotEmpty) { chatCtrl.sendTextMessage(targetUserId, text, isGroup: isGroupMessage); textController.clear(); if (isGroupMessage == true) { chatCtrl.sendGroupChatNotification( notificationMessage: text, notificationTitle: "", notificationType: ApiEnum.ChatNotification, tribeId: targetUserId, ); } if (!isGroupMessage) { chatCtrl.sendChatNotification( notificationMessage: text, notificationTitle: "", notificationType: ApiEnum.ChatNotification, receiverId: targetUserId, ); } } } } class FileUploadLoader extends StatelessWidget { final String targetUserId; final ChatController chatCtrl; const FileUploadLoader({ super.key, required this.targetUserId, required this.chatCtrl, }); @override Widget build(BuildContext context) { return Obx(() { final isUploading = chatCtrl.isSending[targetUserId] ?? false; return AnimatedSwitcher( duration: const Duration(milliseconds: 250), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeIn, transitionBuilder: (child, animation) { return SizeTransition(sizeFactor: animation, child: child); }, child: isUploading ? Container( key: const ValueKey("file_uploading"), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: const Color(primaryColor).withValues(alpha: 0.1), border: Border( top: BorderSide(color: Colors.grey.shade300), ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), offset: const Offset(0, -2), blurRadius: 6, ), ], ), child: Padding( padding: EdgeInsets.all(6), child: Row( children: [ SizedBox( width: 24.w, height: 24.h, child: CircularProgressIndicator( strokeWidth: 2.5, color: const Color(primaryColor), backgroundColor: const Color( primaryColor, ).withValues(alpha: 0.3), ), ), const SizedBox(width: 12), Expanded( child: Text( "Sending file...", style: TextStyle( color: const Color(primaryColor), fontSize: 14, fontWeight: FontWeight.w500, ), ), ), // Tooltip( // message: "Cancel upload", // child: IconButton( // onPressed: () { // chatCtrl.isSending[targetUserId] = false; // }, // icon: const Icon(Icons.close, size: 20), // color: Colors.grey.shade600, // constraints: const BoxConstraints( // minWidth: 32, // minHeight: 32, // ), // ), // ), ], ), ), ) : const SizedBox.shrink(), ); }); } }