feat: Implement clear chat, delete conversation, mute, and report options within the chat screen.
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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,24 +489,39 @@ 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,
|
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
||||||
child: Icon(
|
|
||||||
Icons.more_vert,
|
Icons.more_vert,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: AppColors.primaryDark,
|
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: () {},
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.star_rounded,
|
Icons.star_rounded,
|
||||||
@@ -410,7 +529,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
|
|||||||
color: AppColors.accentOrange,
|
color: AppColors.accentOrange,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user