import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:shimmer/shimmer.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/messaging/data/models/messaging_models.dart'; import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart'; class ConversationsScreen extends ConsumerStatefulWidget { const ConversationsScreen({super.key}); @override ConsumerState createState() => _ConversationsScreenState(); } class _ConversationsScreenState extends ConsumerState { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; bool _isSearchActive = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(messagingProvider.notifier).loadConversations(); }); _searchController.addListener(() { setState(() { _searchQuery = _searchController.text.trim().toLowerCase(); }); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } List _filteredConversations(List conversations) { if (_searchQuery.isEmpty) return conversations; return conversations .where((c) => c.otherParty.name.toLowerCase().contains(_searchQuery)) .toList(); } @override Widget build(BuildContext context) { final state = ref.watch(messagingProvider); final filtered = _filteredConversations(state.conversations); return Column( children: [ _buildHeader(), Expanded(child: _buildConversationList(state, filtered)), ], ); } // -- Header with back arrow, search, icons -- Widget _buildHeader() { return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: Row( children: [ // Back arrow (outside the search box) GestureDetector( onTap: () { if (_isSearchActive) { setState(() { _isSearchActive = false; _searchController.clear(); }); } else { context.go('/home'); } }, behavior: HitTestBehavior.opaque, child: const Padding( padding: EdgeInsets.only(right: 10), child: Icon( Icons.arrow_back_ios_new, size: 16, color: AppColors.primaryDark, ), ), ), // Search box with border Expanded( child: GestureDetector( onTap: () { if (!_isSearchActive) { setState(() => _isSearchActive = true); } }, child: SizedBox( height: 44, child: _isSearchActive ? TextField( controller: _searchController, autofocus: true, style: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), decoration: InputDecoration( hintText: 'Search Messages', hintStyle: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.hintText, ), contentPadding: const EdgeInsets.symmetric( horizontal: 18, vertical: 12, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(22), borderSide: BorderSide( color: AppColors.primaryDark .withValues(alpha: 0.25), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(22), borderSide: BorderSide( color: AppColors.primaryDark .withValues(alpha: 0.25), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(22), borderSide: BorderSide( color: AppColors.primaryDark .withValues(alpha: 0.4), ), ), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.close, size: 18), color: AppColors.hintText, onPressed: () { _searchController.clear(); setState(() {}); }, ) : null, ), ) : Container( alignment: Alignment.centerLeft, decoration: BoxDecoration( border: Border.all( color: AppColors.primaryDark .withValues(alpha: 0.25), ), borderRadius: BorderRadius.circular(22), ), padding: const EdgeInsets.symmetric(horizontal: 18), child: const Text( 'Search Messages', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), ), ), ), ), const SizedBox(width: 10), // Search icon (outside the search box) GestureDetector( onTap: () { setState(() { if (_isSearchActive) { _isSearchActive = false; _searchController.clear(); } else { _isSearchActive = true; } }); }, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.all(4), child: Icon( _isSearchActive ? Icons.close : Icons.search, size: 22, color: AppColors.primaryDark, ), ), ), const SizedBox(width: 8), // Three dots menu (outside the search box) GestureDetector( onTap: () {}, behavior: HitTestBehavior.opaque, child: const Padding( padding: EdgeInsets.all(4), child: Icon( Icons.more_horiz, size: 22, color: AppColors.primaryDark, ), ), ), ], ), ); } // -- Shimmer Loading State -- Widget _buildShimmerLoading() { return Shimmer.fromColors( baseColor: const Color(0xFFE8E8E8), highlightColor: const Color(0xFFF5F5F5), child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), physics: const NeverScrollableScrollPhysics(), itemCount: 6, itemBuilder: (_, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( children: [ Container( width: 56, height: 56, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 130, height: 14, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 8), Container( width: double.infinity, height: 12, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ], ), ), const SizedBox(width: 8), Container( width: 55, height: 12, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ], ), ), ), ); } // -- Conversation List -- Widget _buildConversationList( MessagingState state, List filtered) { if (state.isLoadingConversations) { return _buildShimmerLoading(); } if (filtered.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.chat_bubble_outline_rounded, color: AppColors.hintText, size: 56, ), const SizedBox(height: 16), Text( _searchQuery.isNotEmpty ? 'No conversations found' : 'No messages yet', style: const TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), Text( _searchQuery.isNotEmpty ? 'Try a different search term' : 'Start a conversation with a professional', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.subtleText, ), ), if (state.error != null) ...[ const SizedBox(height: 20), TextButton.icon( onPressed: () => ref.read(messagingProvider.notifier).loadConversations(), icon: const Icon( Icons.refresh, color: AppColors.accentOrange, size: 18, ), label: const Text( 'Retry', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.accentOrange, ), ), ), ], ], ), ); } return RefreshIndicator( color: AppColors.accentOrange, onRefresh: () => ref.read(messagingProvider.notifier).loadConversations(), child: ListView.separated( padding: EdgeInsets.zero, itemCount: filtered.length, separatorBuilder: (context, index) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Divider( color: AppColors.primaryDark.withValues(alpha: 0.1), height: 1, thickness: 1, ), ), itemBuilder: (context, index) { return _ConversationTile( conversation: filtered[index], onTap: () => context.go('/messages/chat/${filtered[index].id}'), ); }, ), ); } } // -- Conversation Tile Widget -- class _ConversationTile extends StatelessWidget { final Conversation conversation; final VoidCallback onTap; const _ConversationTile({ required this.conversation, required this.onTap, }); 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'; } @override Widget build(BuildContext context) { final otherParty = conversation.otherParty; final avatarUrl = _resolveAvatarUrl(otherParty.avatar); final lastMessage = conversation.lastMessageText ?? ''; final timestamp = _formatTimestamp(conversation.lastMessageAt); final unread = conversation.unreadCount; final firstLetter = otherParty.name.isNotEmpty ? otherParty.name[0].toUpperCase() : '?'; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Avatar with online indicator Stack( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.15), width: 1.5, ), ), child: ClipOval( child: avatarUrl.isNotEmpty ? CachedNetworkImage( imageUrl: avatarUrl, width: 56, height: 56, fit: BoxFit.cover, placeholder: (context, url) => _AvatarFallback( letter: firstLetter, ), errorWidget: (context, url, err) => _AvatarFallback( letter: firstLetter, ), ) : _AvatarFallback(letter: firstLetter), ), ), if (otherParty.isOnline) Positioned( right: 1, bottom: 1, child: Container( width: 14, height: 14, decoration: BoxDecoration( color: const Color(0xFF4CAF50), shape: BoxShape.circle, border: Border.all( color: Colors.white, width: 2.5, ), ), ), ), ], ), const SizedBox(width: 14), // Name and last message Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Name row with optional unread badge Row( children: [ Expanded( child: Text( otherParty.name, style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: unread > 0 ? FontWeight.w700 : FontWeight.w600, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (unread > 0) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2), decoration: BoxDecoration( color: AppColors.accentOrange, borderRadius: BorderRadius.circular(10), ), child: Text( '$unread', style: const TextStyle( fontFamily: 'Fractul', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ], ], ), const SizedBox(height: 4), Text( lastMessage, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w400, color: unread > 0 ? AppColors.primaryDark : AppColors.hintText, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 10), // Timestamp Text( timestamp, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w400, color: unread > 0 ? AppColors.primaryDark : AppColors.hintText, ), ), ], ), ), ); } } // -- Avatar Fallback -- class _AvatarFallback extends StatelessWidget { final String letter; const _AvatarFallback({required this.letter}); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: const BoxDecoration( color: Color(0xFFE8E8E8), shape: BoxShape.circle, ), alignment: Alignment.center, child: Text( letter, style: const TextStyle( fontFamily: 'Fractul', fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ); } } // -- Timestamp Formatting -- String _formatTimestamp(String? isoTimestamp) { if (isoTimestamp == null || isoTimestamp.isEmpty) return ''; final dateTime = DateTime.tryParse(isoTimestamp); if (dateTime == null) return ''; final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final messageDate = DateTime(dateTime.year, dateTime.month, dateTime.day); final localTime = dateTime.toLocal(); final diff = now.difference(dateTime); // "Now" for messages within the last 2 minutes if (diff.inMinutes < 2) { return 'Now'; } // Same day: show relative time if (messageDate == today) { if (diff.inHours >= 1) { return '${diff.inHours} hour${diff.inHours > 1 ? 's' : ''} Ago'; } return '${diff.inMinutes} min Ago'; } // Within last 30 days: show "X Days Ago" final daysDiff = today.difference(messageDate).inDays; if (daysDiff == 1) { return 'Yesterday'; } if (daysDiff <= 30) { return '$daysDiff Days Ago'; } // Older: show month and day const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; return '${months[localTime.month - 1]} ${localTime.day}'; }