feat: Implement clear chat, delete conversation, mute, and report options within the chat screen.

This commit is contained in:
pradeepkumar
2026-03-19 13:57:14 +05:30
parent b848b0260b
commit a837e5ac12
5 changed files with 217 additions and 23 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.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/api_constants.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/core/network/api_client.dart'; import 'package:real_estate_mobile/core/network/api_client.dart';
@@ -27,6 +28,7 @@ class _ContactScreenState extends State<ContactScreen> {
String _contactOfficeCity = 'New York CA 234737'; String _contactOfficeCity = 'New York CA 234737';
String _pageTitle = 'Get In Touch'; 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 _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 @override
void initState() { void initState() {
@@ -53,6 +55,7 @@ class _ContactScreenState extends State<ContactScreen> {
_contactOfficeCity = content['officeCity'] as String? ?? _contactOfficeCity; _contactOfficeCity = content['officeCity'] as String? ?? _contactOfficeCity;
_pageTitle = content['title'] as String? ?? _pageTitle; _pageTitle = content['title'] as String? ?? _pageTitle;
_pageDescription = content['description'] as String? ?? _pageDescription; _pageDescription = content['description'] as String? ?? _pageDescription;
_directionsUrl = content['directionsUrl'] as String? ?? _directionsUrl;
}); });
} }
} }
@@ -443,7 +446,12 @@ class _ContactScreenState extends State<ContactScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Get Directions button // Get Directions button
GestureDetector( GestureDetector(
onTap: () {}, onTap: () async {
final uri = Uri.parse(_directionsUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
child: Container( child: Container(
width: 139, width: 139,
height: 35, height: 35,

View File

@@ -160,6 +160,32 @@ class MessagingRepository {
); );
} }
} }
/// DELETE /messages/conversations/:id/messages — clear all messages
Future<void> 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<void> 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<MessagingRepository>((ref) { final messagingRepositoryProvider = Provider<MessagingRepository>((ref) {

View File

@@ -527,6 +527,48 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
return null; return null;
} }
} }
Future<void> clearChat(String conversationId) async {
try {
await _repository.clearChat(conversationId);
// Clear local messages
final updatedMessages =
Map<String, List<ChatMessage>>.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<void> deleteConversation(String conversationId) async {
try {
await _repository.deleteConversation(conversationId);
// Remove from local state
final updatedMessages =
Map<String, List<ChatMessage>>.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 = final messagingProvider =

View File

@@ -29,6 +29,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final FocusNode _messageFocusNode = FocusNode(); final FocusNode _messageFocusNode = FocusNode();
bool _isComposing = false; bool _isComposing = false;
bool _isMuted = false;
int _previousMessageCount = 0; int _previousMessageCount = 0;
final SocketService _socket = SocketService(); final SocketService _socket = SocketService();
@@ -145,6 +146,107 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
}); });
} }
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() { void _handleBack() {
ref.read(messagingProvider.notifier).setActiveConversation(null); ref.read(messagingProvider.notifier).setActiveConversation(null);
context.go('/messages'); context.go('/messages');
@@ -255,6 +357,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
name: otherPartyName, name: otherPartyName,
isOnline: isOnline, isOnline: isOnline,
avatar: otherPartyAvatar, avatar: otherPartyAvatar,
agentProfileId: conversation?.agentProfileId,
), ),
Expanded( Expanded(
child: messagingState.isLoadingMessages && messages.isEmpty child: messagingState.isLoadingMessages && messages.isEmpty
@@ -289,6 +392,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
required String name, required String name,
required bool isOnline, required bool isOnline,
String? avatar, String? avatar,
String? agentProfileId,
}) { }) {
final initials = _getInitials(name); final initials = _getInitials(name);
@@ -385,30 +489,44 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
], ],
), ),
), ),
// Three dots (vertical) // Three dots menu (matches web: Clear Chat, Mute, Report, Delete)
GestureDetector( PopupMenuButton<String>(
onTap: () {}, icon: const Icon(
behavior: HitTestBehavior.opaque, Icons.more_vert,
child: const Padding( size: 20,
padding: EdgeInsets.symmetric(horizontal: 10), color: AppColors.primaryDark,
child: 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 // Star icon
GestureDetector( Padding(
onTap: () {}, padding: const EdgeInsets.only(right: 4),
behavior: HitTestBehavior.opaque, child: Icon(
child: Padding( Icons.star_rounded,
padding: const EdgeInsets.only(right: 4), size: 24,
child: Icon( color: AppColors.accentOrange,
Icons.star_rounded,
size: 24,
color: AppColors.accentOrange,
),
), ),
), ),
], ],

View File

@@ -366,7 +366,7 @@ class _ProfileSettingsTabState extends ConsumerState<ProfileSettingsTab> {
final location = _locationController.text.trim(); final location = _locationController.text.trim();
if (location.isNotEmpty) data['serviceAreas'] = [location]; if (location.isNotEmpty) data['serviceAreas'] = [location];
} else { } else {
data['headline'] = _titleController.text.trim(); // Note: headline is not a field on UserProfile — don't send it
final locationParts = _locationController.text final locationParts = _locationController.text
.split(',') .split(',')
.map((s) => s.trim()) .map((s) => s.trim())