From a837e5ac1223270bd60adc3fe39226da0ee4e84d Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Thu, 19 Mar 2026 13:57:14 +0530 Subject: [PATCH] feat: Implement clear chat, delete conversation, mute, and report options within the chat screen. --- .../presentation/screens/contact_screen.dart | 10 +- .../messaging/data/messaging_repository.dart | 26 +++ .../providers/messaging_provider.dart | 42 +++++ .../presentation/screens/chat_screen.dart | 160 +++++++++++++++--- .../widgets/profile_settings_tab.dart | 2 +- 5 files changed, 217 insertions(+), 23 deletions(-) diff --git a/lib/features/contact/presentation/screens/contact_screen.dart b/lib/features/contact/presentation/screens/contact_screen.dart index 440cfe8..2393142 100644 --- a/lib/features/contact/presentation/screens/contact_screen.dart +++ b/lib/features/contact/presentation/screens/contact_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:real_estate_mobile/core/constants/api_constants.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/network/api_client.dart'; @@ -27,6 +28,7 @@ class _ContactScreenState extends State { String _contactOfficeCity = 'New York CA 234737'; String _pageTitle = 'Get In Touch'; String _pageDescription = 'Have a question about a property or need assistance? Fill out the form below and our team will get back to you shortly.'; + String _directionsUrl = 'https://maps.google.com/?q=123+Market+Street+New+York+CA+234737'; @override void initState() { @@ -53,6 +55,7 @@ class _ContactScreenState extends State { _contactOfficeCity = content['officeCity'] as String? ?? _contactOfficeCity; _pageTitle = content['title'] as String? ?? _pageTitle; _pageDescription = content['description'] as String? ?? _pageDescription; + _directionsUrl = content['directionsUrl'] as String? ?? _directionsUrl; }); } } @@ -443,7 +446,12 @@ class _ContactScreenState extends State { const SizedBox(height: 16), // Get Directions button GestureDetector( - onTap: () {}, + onTap: () async { + final uri = Uri.parse(_directionsUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + }, child: Container( width: 139, height: 35, diff --git a/lib/features/messaging/data/messaging_repository.dart b/lib/features/messaging/data/messaging_repository.dart index a7dfd03..d1e4965 100644 --- a/lib/features/messaging/data/messaging_repository.dart +++ b/lib/features/messaging/data/messaging_repository.dart @@ -160,6 +160,32 @@ class MessagingRepository { ); } } + + /// DELETE /messages/conversations/:id/messages — clear all messages + Future clearChat(String conversationId) async { + try { + await _dio.delete('${ApiConstants.conversations}/$conversationId/messages'); + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException( + message: e.message ?? 'Failed to clear chat', + statusCode: e.response?.statusCode, + ); + } + } + + /// DELETE /messages/conversations/:id — delete conversation entirely + Future deleteConversation(String conversationId) async { + try { + await _dio.delete('${ApiConstants.conversations}/$conversationId'); + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException( + message: e.message ?? 'Failed to delete conversation', + statusCode: e.response?.statusCode, + ); + } + } } final messagingRepositoryProvider = Provider((ref) { diff --git a/lib/features/messaging/presentation/providers/messaging_provider.dart b/lib/features/messaging/presentation/providers/messaging_provider.dart index e6a09aa..d0343e9 100644 --- a/lib/features/messaging/presentation/providers/messaging_provider.dart +++ b/lib/features/messaging/presentation/providers/messaging_provider.dart @@ -527,6 +527,48 @@ class MessagingNotifier extends StateNotifier { return null; } } + + Future clearChat(String conversationId) async { + try { + await _repository.clearChat(conversationId); + // Clear local messages + final updatedMessages = + Map>.from(state.messages) + ..[conversationId] = []; + // Update conversation preview + final updatedConversations = state.conversations.map((c) { + if (c.id == conversationId) { + return c.copyWith(lastMessageText: '', unreadCount: 0); + } + return c; + }).toList(); + state = state.copyWith( + messages: updatedMessages, + conversations: updatedConversations, + ); + } catch (_) { + state = state.copyWith(error: 'Failed to clear chat'); + } + } + + Future deleteConversation(String conversationId) async { + try { + await _repository.deleteConversation(conversationId); + // Remove from local state + final updatedMessages = + Map>.from(state.messages) + ..remove(conversationId); + final updatedConversations = + state.conversations.where((c) => c.id != conversationId).toList(); + state = state.copyWith( + messages: updatedMessages, + conversations: updatedConversations, + clearActiveConversation: true, + ); + } catch (_) { + state = state.copyWith(error: 'Failed to delete conversation'); + } + } } final messagingProvider = diff --git a/lib/features/messaging/presentation/screens/chat_screen.dart b/lib/features/messaging/presentation/screens/chat_screen.dart index a44a5c5..0513e82 100644 --- a/lib/features/messaging/presentation/screens/chat_screen.dart +++ b/lib/features/messaging/presentation/screens/chat_screen.dart @@ -29,6 +29,7 @@ class _ChatScreenState extends ConsumerState final ScrollController _scrollController = ScrollController(); final FocusNode _messageFocusNode = FocusNode(); bool _isComposing = false; + bool _isMuted = false; int _previousMessageCount = 0; final SocketService _socket = SocketService(); @@ -145,6 +146,107 @@ class _ChatScreenState extends ConsumerState }); } + void _handleMenuAction(String action, String? agentProfileId) { + switch (action) { + case 'clear_chat': + _showConfirmDialog( + title: 'Clear Chat', + message: 'Are you sure you want to clear all messages? This cannot be undone.', + onConfirm: () { + ref.read(messagingProvider.notifier).clearChat(widget.conversationId); + }, + ); + break; + case 'mute': + setState(() => _isMuted = !_isMuted); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(_isMuted ? 'Notifications muted' : 'Notifications unmuted'), + duration: const Duration(seconds: 2), + ), + ); + break; + case 'report': + _showReportDialog(); + break; + case 'delete': + _showConfirmDialog( + title: 'Delete Conversation', + message: 'Are you sure you want to delete this conversation? This cannot be undone.', + onConfirm: () { + ref.read(messagingProvider.notifier).deleteConversation(widget.conversationId); + context.go('/messages'); + }, + isDanger: true, + ); + break; + } + } + + void _showConfirmDialog({ + required String title, + required String message, + required VoidCallback onConfirm, + bool isDanger = false, + }) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + onConfirm(); + }, + child: Text( + 'Confirm', + style: TextStyle(color: isDanger ? Colors.red : null), + ), + ), + ], + ), + ); + } + + void _showReportDialog() { + final controller = TextEditingController(); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Report User'), + content: TextField( + controller: controller, + maxLines: 3, + decoration: const InputDecoration( + hintText: 'Describe the issue...', + border: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Report submitted. Thank you.')), + ); + }, + child: const Text('Submit'), + ), + ], + ), + ); + } + void _handleBack() { ref.read(messagingProvider.notifier).setActiveConversation(null); context.go('/messages'); @@ -255,6 +357,7 @@ class _ChatScreenState extends ConsumerState name: otherPartyName, isOnline: isOnline, avatar: otherPartyAvatar, + agentProfileId: conversation?.agentProfileId, ), Expanded( child: messagingState.isLoadingMessages && messages.isEmpty @@ -289,6 +392,7 @@ class _ChatScreenState extends ConsumerState required String name, required bool isOnline, String? avatar, + String? agentProfileId, }) { final initials = _getInitials(name); @@ -385,30 +489,44 @@ class _ChatScreenState extends ConsumerState ], ), ), - // 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, - ), + // Three dots menu (matches web: Clear Chat, Mute, Report, Delete) + PopupMenuButton( + icon: const Icon( + Icons.more_vert, + size: 20, + color: AppColors.primaryDark, ), + padding: EdgeInsets.zero, + onSelected: (value) => _handleMenuAction(value, agentProfileId), + itemBuilder: (_) => [ + const PopupMenuItem( + value: 'clear_chat', + child: Text('Clear Chat'), + ), + PopupMenuItem( + value: 'mute', + child: Text(_isMuted ? 'Unmute Notifications' : 'Mute Notifications'), + ), + const PopupMenuItem( + value: 'report', + child: Text('Report User'), + ), + const PopupMenuItem( + value: 'delete', + child: Text( + 'Delete Conversation', + style: TextStyle(color: Colors.red), + ), + ), + ], ), // Star icon - GestureDetector( - onTap: () {}, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon( - Icons.star_rounded, - size: 24, - color: AppColors.accentOrange, - ), + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.star_rounded, + size: 24, + color: AppColors.accentOrange, ), ), ], diff --git a/lib/features/profile/presentation/widgets/profile_settings_tab.dart b/lib/features/profile/presentation/widgets/profile_settings_tab.dart index 7138981..9455908 100644 --- a/lib/features/profile/presentation/widgets/profile_settings_tab.dart +++ b/lib/features/profile/presentation/widgets/profile_settings_tab.dart @@ -366,7 +366,7 @@ class _ProfileSettingsTabState extends ConsumerState { final location = _locationController.text.trim(); if (location.isNotEmpty) data['serviceAreas'] = [location]; } else { - data['headline'] = _titleController.text.trim(); + // Note: headline is not a field on UserProfile — don't send it final locationParts = _locationController.text .split(',') .map((s) => s.trim())