import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:real_estate_mobile/config/app_config.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart'; import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart'; class ChatScreen extends ConsumerStatefulWidget { final String conversationId; const ChatScreen({super.key, required this.conversationId}); @override ConsumerState createState() => _ChatScreenState(); } class _ChatScreenState extends ConsumerState { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final FocusNode _messageFocusNode = FocusNode(); bool _isComposing = false; int _previousMessageCount = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final notifier = ref.read(messagingProvider.notifier); notifier.setActiveConversation(widget.conversationId); notifier.loadMessages(widget.conversationId); notifier.markAsRead(widget.conversationId); }); _scrollController.addListener(_onScroll); _messageController.addListener(_onTextChanged); } @override void dispose() { ref.read(messagingProvider.notifier).setActiveConversation(null); _messageController.dispose(); _scrollController.dispose(); _messageFocusNode.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 50) { ref .read(messagingProvider.notifier) .loadMessages(widget.conversationId, loadMore: true); } } void _onTextChanged() { final composing = _messageController.text.trim().isNotEmpty; if (composing != _isComposing) { setState(() => _isComposing = composing); } } void _scrollToBottom({bool animated = true}) { if (!_scrollController.hasClients) return; if (animated) { _scrollController.animateTo( 0.0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } else { _scrollController.jumpTo(0.0); } } void _sendMessage() { final text = _messageController.text.trim(); if (text.isEmpty) return; ref .read(messagingProvider.notifier) .sendMessage(widget.conversationId, text); _messageController.clear(); setState(() => _isComposing = false); Future.delayed(const Duration(milliseconds: 100), () { _scrollToBottom(); }); } void _handleBack() { ref.read(messagingProvider.notifier).setActiveConversation(null); Navigator.pop(context); } // ============================================================ // DATE / TIME FORMATTING // ============================================================ String _formatTime(String isoTimestamp) { try { final dt = DateTime.parse(isoTimestamp).toLocal(); final hour = dt.hour > 12 ? dt.hour - 12 : (dt.hour == 0 ? 12 : dt.hour); final minute = dt.minute.toString().padLeft(2, '0'); return '$hour.$minute'; } catch (_) { return ''; } } String _formatDateSeparator(DateTime date) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final messageDate = DateTime(date.year, date.month, date.day); final diff = today.difference(messageDate).inDays; if (diff == 0) return 'Today'; if (diff == 1) return 'Yesterday'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } bool _shouldShowDateSeparator(List messages, int index) { if (index == messages.length - 1) return true; final current = DateTime.parse(messages[index].createdAt).toLocal(); final older = DateTime.parse(messages[index + 1].createdAt).toLocal(); return DateTime(current.year, current.month, current.day) != DateTime(older.year, older.month, older.day); } String _getInitials(String name) { final parts = name.trim().split(RegExp(r'\s+')); if (parts.isEmpty) return ''; if (parts.length == 1) return parts[0][0].toUpperCase(); return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); } String _resolveAvatarUrl(String? avatarKey) { if (avatarKey == null || avatarKey.isEmpty) return ''; if (avatarKey.startsWith('http://') || avatarKey.startsWith('https://')) { return avatarKey; } final base = AppConfig.storageBaseUrl; if (avatarKey.startsWith('/uploads')) { return '$base$avatarKey'; } return '$base/$avatarKey'; } // ============================================================ // BUILD // ============================================================ @override Widget build(BuildContext context) { final messagingState = ref.watch(messagingProvider); final currentUserId = ref.read(authProvider).user?.id; final messages = messagingState.messages[widget.conversationId] ?? []; final isTyping = messagingState.typingUsers.isNotEmpty; final conversation = messagingState.conversations .cast() .firstWhere( (c) => c!.id == widget.conversationId, orElse: () => null, ); if (messages.length > _previousMessageCount && _previousMessageCount > 0) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(animated: true); }); } _previousMessageCount = messages.length; final otherPartyName = conversation?.otherParty.name ?? 'Chat'; final otherPartyAvatar = conversation?.otherParty.avatar; final isOnline = conversation?.otherParty.isOnline ?? false; return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( children: [ _buildHeader( name: otherPartyName, isOnline: isOnline, ), Expanded( child: messagingState.isLoadingMessages && messages.isEmpty ? const Center( child: CircularProgressIndicator( color: AppColors.accentOrange, strokeWidth: 2, ), ) : messages.isEmpty ? Center( child: Text( 'No messages yet.\nSay hello!', textAlign: TextAlign.center, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.hintText, ), ), ) : _buildMessagesList( messages: messages, currentUserId: currentUserId ?? '', currentUserName: 'You', otherPartyName: otherPartyName, otherPartyAvatar: otherPartyAvatar, isTyping: isTyping, isLoadingMore: messagingState.isLoadingMessages && messages.isNotEmpty, ), ), _buildInputArea(), ], ), ), ); } // ============================================================ // HEADER - matches Figma: rounded border, back arrow, name, status, icons // ============================================================ Widget _buildHeader({ required String name, required bool isOnline, }) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Container( height: 55, decoration: BoxDecoration( border: Border.all( color: AppColors.primaryDark, width: 0.5, ), borderRadius: BorderRadius.circular(7), ), child: Row( children: [ // Back arrow GestureDetector( onTap: _handleBack, behavior: HitTestBehavior.opaque, child: const Padding( padding: EdgeInsets.symmetric(horizontal: 14), child: Icon( Icons.arrow_back, size: 23, color: AppColors.primaryDark, ), ), ), // Name and active status Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Row( children: [ if (isOnline) ...[ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Color(0xFF4CAF50), shape: BoxShape.circle, ), ), const SizedBox(width: 5), ], Text( isOnline ? 'Active Now' : 'Offline', style: const TextStyle( fontFamily: 'Fractul', fontSize: 10, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ], ), ], ), ), // Three dots (vertical) GestureDetector( onTap: () {}, behavior: HitTestBehavior.opaque, child: const Padding( padding: EdgeInsets.symmetric(horizontal: 10), child: Icon( Icons.more_vert, size: 20, color: AppColors.primaryDark, ), ), ), // Star icon GestureDetector( onTap: () {}, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.only(right: 14), child: Icon( Icons.star_rounded, size: 24, color: AppColors.accentOrange, ), ), ), ], ), ), ); } // ============================================================ // MESSAGES LIST // ============================================================ Widget _buildMessagesList({ required List messages, required String currentUserId, required String currentUserName, required String otherPartyName, String? otherPartyAvatar, required bool isTyping, required bool isLoadingMore, }) { final itemCount = messages.length + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0); return ListView.builder( controller: _scrollController, reverse: true, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), itemCount: itemCount, itemBuilder: (context, index) { if (isTyping && index == 0) { return _buildTypingIndicator(); } final adjustedIndex = isTyping ? index - 1 : index; if (isLoadingMore && adjustedIndex == messages.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: AppColors.accentOrange, strokeWidth: 2, ), ), ), ); } if (adjustedIndex < 0 || adjustedIndex >= messages.length) { return const SizedBox.shrink(); } final message = messages[adjustedIndex]; final isMine = message.isMine(currentUserId); final showDateSeparator = _shouldShowDateSeparator(messages, adjustedIndex); // Get sender info final senderName = isMine ? (message.sender.displayName.isNotEmpty ? message.sender.displayName : currentUserName) : otherPartyName; final senderAvatar = isMine ? null : otherPartyAvatar; return Column( children: [ if (showDateSeparator) _buildDateSeparator( DateTime.parse(message.createdAt).toLocal(), ), Padding( padding: const EdgeInsets.only(bottom: 16), child: isMine ? _buildSentMessage(message, senderName) : _buildReceivedMessage(message, senderName, senderAvatar), ), ], ); }, ); } // -- Date Separator -- Widget _buildDateSeparator(DateTime date) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ Expanded( child: Divider( color: AppColors.primaryDark.withValues(alpha: 0.1), thickness: 0.5, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( _formatDateSeparator(date), style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 12, fontWeight: FontWeight.w400, color: AppColors.subtleText, ), ), ), Expanded( child: Divider( color: AppColors.primaryDark.withValues(alpha: 0.1), thickness: 0.5, ), ), ], ), ); } // -- Sent Message (right-aligned, with avatar on right) -- Widget _buildSentMessage(ChatMessage message, String senderName) { final timestamp = _formatTime(message.createdAt); return Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(), Flexible( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ // Name row with timestamp Row( mainAxisSize: MainAxisSize.min, children: [ Text( timestamp, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w200, color: AppColors.primaryDark, ), ), const SizedBox(width: 6), Text( senderName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(width: 6), Container( width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.accentOrange, shape: BoxShape.circle, ), ), ], ), const SizedBox(height: 6), // Message text Text( message.content, style: const TextStyle( fontFamily: 'Fractul', fontSize: 10, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), textAlign: TextAlign.right, ), ], ), ), const SizedBox(width: 10), // Avatar _buildMessageAvatar(null, senderName, 45), ], ); } // -- Received Message (left-aligned, with avatar on left) -- Widget _buildReceivedMessage( ChatMessage message, String senderName, String? avatar) { final timestamp = _formatTime(message.createdAt); return Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Avatar _buildMessageAvatar(avatar, senderName, 45), const SizedBox(width: 10), Flexible( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Name row with timestamp Row( mainAxisSize: MainAxisSize.min, children: [ Text( senderName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(width: 6), Container( width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.accentOrange, shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( timestamp, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w200, color: AppColors.primaryDark, ), ), ], ), const SizedBox(height: 6), // Message text Text( message.content, style: const TextStyle( fontFamily: 'Fractul', fontSize: 10, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ], ), ), const Spacer(), ], ); } // -- Message Avatar -- Widget _buildMessageAvatar(String? avatarKey, String name, double size) { final avatarUrl = _resolveAvatarUrl(avatarKey); final initials = _getInitials(name); return ClipOval( child: avatarUrl.isNotEmpty ? CachedNetworkImage( imageUrl: avatarUrl, width: size, height: size, fit: BoxFit.cover, errorWidget: (_, __, ___) => _AvatarFallback(letter: initials, size: size), ) : _AvatarFallback(letter: initials, size: size), ); } // -- Typing Indicator -- Widget _buildTypingIndicator() { return Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ _buildDot(0), const SizedBox(width: 4), _buildDot(1), const SizedBox(width: 4), _buildDot(2), ], ), ), ); } Widget _buildDot(int index) { return TweenAnimationBuilder( tween: Tween(begin: 0.3, end: 1.0), duration: Duration(milliseconds: 600 + (index * 200)), builder: (context, value, child) { return Opacity( opacity: value, child: Container( width: 6, height: 6, decoration: const BoxDecoration( color: AppColors.subtleText, shape: BoxShape.circle, ), ), ); }, ); } // ============================================================ // INPUT AREA - matches Figma: + circle, text input, send, mic circle // ============================================================ Widget _buildInputArea() { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), color: Colors.white, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Plus icon in circle border GestureDetector( onTap: _showAttachmentOptions, child: Container( width: 36, height: 36, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, border: Border.all( color: AppColors.primaryDark, width: 0.5, ), ), child: const Center( child: Icon( Icons.add, size: 24, color: AppColors.primaryDark, ), ), ), ), const SizedBox(width: 10), // Text input Expanded( child: Container( height: 38, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: AppColors.primaryDark, width: 0.5, ), ), child: Row( children: [ Expanded( child: TextField( controller: _messageController, focusNode: _messageFocusNode, textInputAction: TextInputAction.send, onSubmitted: (_) => _sendMessage(), style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), decoration: const InputDecoration( hintText: 'Write a Message', hintStyle: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), contentPadding: EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, ), ), ), // Send icon inside the input field GestureDetector( onTap: _sendMessage, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.only(right: 12), child: Icon( Icons.send, size: 20, color: AppColors.accentOrange, ), ), ), ], ), ), ), const SizedBox(width: 10), // Mic icon in circle border GestureDetector( onTap: () => _showComingSoon('Speech to text'), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, border: Border.all( color: AppColors.primaryDark, width: 0.5, ), ), child: const Center( child: Icon( Icons.mic_none, size: 20, color: AppColors.primaryDark, ), ), ), ), ], ), ); } // ============================================================ // HELPERS // ============================================================ void _showComingSoon(String feature) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$feature coming soon'), duration: const Duration(seconds: 1), backgroundColor: AppColors.primaryDark, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ); } void _showAttachmentOptions() { showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { return Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, decoration: BoxDecoration( color: AppColors.divider, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 20), _buildAttachmentOption( icon: Icons.image_outlined, label: 'Photo', onTap: () { Navigator.pop(ctx); _showComingSoon('Photo attachment'); }, ), _buildAttachmentOption( icon: Icons.attach_file, label: 'Document', onTap: () { Navigator.pop(ctx); _showComingSoon('Document attachment'); }, ), _buildAttachmentOption( icon: Icons.location_on_outlined, label: 'Location', onTap: () { Navigator.pop(ctx); _showComingSoon('Location sharing'); }, ), const SizedBox(height: 10), ], ), ); }, ); } Widget _buildAttachmentOption({ required IconData icon, required String label, required VoidCallback onTap, }) { return ListTile( leading: Icon(icon, color: AppColors.primaryDark), title: Text( label, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), onTap: onTap, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ); } } // -- Avatar Fallback -- class _AvatarFallback extends StatelessWidget { final String letter; final double size; const _AvatarFallback({required this.letter, this.size = 45}); @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: const BoxDecoration( color: Color(0xFFE8E8E8), shape: BoxShape.circle, ), alignment: Alignment.center, child: Text( letter, style: TextStyle( fontFamily: 'Fractul', fontSize: size * 0.38, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ); } }