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 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), child: Container( height: 51, decoration: BoxDecoration( border: Border.all( color: AppColors.primaryDark, width: 0.5, ), borderRadius: BorderRadius.circular(7), ), child: Row( children: [ // Back arrow GestureDetector( onTap: () { if (_isSearchActive) { setState(() { _isSearchActive = false; _searchController.clear(); }); } else { context.go('/home'); } }, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Icon( Icons.arrow_back_ios_new, size: 17, color: AppColors.primaryDark, ), ), ), // Search text or input Expanded( child: _isSearchActive ? TextField( controller: _searchController, autofocus: true, style: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), decoration: const InputDecoration( hintText: 'Search Messages', hintStyle: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.hintText, ), border: InputBorder.none, contentPadding: EdgeInsets.zero, isDense: true, ), ) : GestureDetector( onTap: () => setState(() => _isSearchActive = true), child: const Text( 'Search Messages', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), ), // Search icon GestureDetector( onTap: () => setState(() => _isSearchActive = true), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( Icons.search, size: 20, color: AppColors.primaryDark, ), ), ), // Three dots menu GestureDetector( onTap: () {}, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.only(right: 12, left: 4), child: Icon( Icons.more_horiz, size: 20, 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: 12), physics: const NeverScrollableScrollPhysics(), itemCount: 6, itemBuilder: (_, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( children: [ Container( width: 52, height: 52, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), const SizedBox(width: 10), 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: 45, 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: (_, __) => Divider( color: AppColors.primaryDark.withValues(alpha: 0.1), height: 0.5, thickness: 0.5, ), 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 firstLetter = otherParty.name.isNotEmpty ? otherParty.name[0].toUpperCase() : '?'; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), child: Row( children: [ // Avatar with online indicator Stack( children: [ ClipOval( child: avatarUrl.isNotEmpty ? CachedNetworkImage( imageUrl: avatarUrl, width: 52, height: 52, fit: BoxFit.cover, placeholder: (_, url) => _AvatarFallback( letter: firstLetter, ), errorWidget: (_, url, err) => _AvatarFallback( letter: firstLetter, ), ) : _AvatarFallback(letter: firstLetter), ), if (otherParty.isOnline) Positioned( right: 0, bottom: 0, 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: 10), // Name and last message Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( otherParty.name, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( lastMessage, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), // Timestamp Text( timestamp, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ], ), ), ); } } // -- Avatar Fallback -- class _AvatarFallback extends StatelessWidget { final String letter; const _AvatarFallback({required this.letter}); @override Widget build(BuildContext context) { return Container( width: 52, height: 52, decoration: const BoxDecoration( color: Color(0xFFE8E8E8), shape: BoxShape.circle, ), alignment: Alignment.center, child: Text( letter, style: const TextStyle( fontFamily: 'Fractul', fontSize: 20, 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 time like "2 hours Ago" 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}'; }