feat: Implement contact form clearing, refactor chat avatar display to use S3Image, add unread count to notification filter, and enhance agent detail layout.

This commit is contained in:
pradeepkumar
2026-03-15 00:03:38 +05:30
parent de4a7b8040
commit fed89cbef2
4 changed files with 97 additions and 67 deletions

View File

@@ -479,6 +479,8 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
fontSize: 14, fontSize: 14,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -502,13 +504,14 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Name + Verified badge + (Verified Expert) — badge after last name // Name + Verified badge + (Verified Expert) — badge after last name
Row( Wrap(
mainAxisAlignment: MainAxisAlignment.center, alignment: WrapAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline, crossAxisAlignment: WrapCrossAlignment.center,
textBaseline: TextBaseline.alphabetic, spacing: 4,
runSpacing: 4,
children: [ children: [
Text( Text(
agent.firstName, '${agent.firstName} ${agent.lastName}',
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 18, fontSize: 18,
@@ -516,28 +519,17 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
const SizedBox(width: 4),
Text(
agent.lastName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
if (agent.isVerified) ...[ if (agent.isVerified) ...[
const SizedBox(width: 6), Row(
const Padding( mainAxisSize: MainAxisSize.min,
padding: EdgeInsets.only(bottom: 1), children: const [
child: Icon( Icon(
Icons.verified, Icons.verified,
size: 16, size: 16,
color: Color(0xFF2196F3), color: Color(0xFF2196F3),
), ),
), SizedBox(width: 4),
const SizedBox(width: 6), Text(
const Text(
'(Verified Expert)', '(Verified Expert)',
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
@@ -547,6 +539,8 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
), ),
), ),
], ],
),
],
], ],
), ),
// Title // Title

View File

@@ -241,7 +241,12 @@ class _ContactScreenState extends State<ContactScreen> {
children: [ children: [
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () => context.pop(), onTap: () {
_nameController.clear();
_emailController.clear();
_phoneController.clear();
_messageController.clear();
},
child: Container( child: Container(
height: 46, height: 46,
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.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/core/constants/app_colors.dart';
import 'package:real_estate_mobile/core/utils/image_url_resolver.dart'; import 'package:real_estate_mobile/core/utils/image_url_resolver.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart';
@@ -149,18 +148,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
} }
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';
}
String _formatFileSize(int? bytes) { String _formatFileSize(int? bytes) {
if (bytes == null || bytes == 0) return ''; if (bytes == null || bytes == 0) return '';
if (bytes < 1024) return '$bytes B'; if (bytes < 1024) return '$bytes B';
@@ -209,6 +196,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
_buildHeader( _buildHeader(
name: otherPartyName, name: otherPartyName,
isOnline: isOnline, isOnline: isOnline,
avatar: otherPartyAvatar,
), ),
Expanded( Expanded(
child: messagingState.isLoadingMessages && messages.isEmpty child: messagingState.isLoadingMessages && messages.isEmpty
@@ -242,7 +230,10 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
Widget _buildHeader({ Widget _buildHeader({
required String name, required String name,
required bool isOnline, required bool isOnline,
String? avatar,
}) { }) {
final initials = _getInitials(name);
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6), padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
child: Row( child: Row(
@@ -252,7 +243,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
onTap: _handleBack, onTap: _handleBack,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: const Padding( child: const Padding(
padding: EdgeInsets.only(right: 16), padding: EdgeInsets.only(right: 12),
child: Icon( child: Icon(
Icons.arrow_back, Icons.arrow_back,
size: 22, size: 22,
@@ -260,6 +251,38 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
), ),
), ),
), ),
// Avatar
Stack(
children: [
ClipOval(
child: (avatar != null && avatar.isNotEmpty)
? S3Image(
imageUrl: avatar,
width: 40,
height: 40,
fit: BoxFit.cover,
errorWidget: (_) =>
_AvatarFallback(letter: initials, size: 40),
)
: _AvatarFallback(letter: initials, size: 40),
),
if (isOnline)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
const SizedBox(width: 10),
// Name and active status // Name and active status
Expanded( Expanded(
child: Column( child: Column(
@@ -398,7 +421,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
String? headline, String? headline,
required bool isOnline, required bool isOnline,
}) { }) {
final avatarUrl = _resolveAvatarUrl(avatar);
final initials = _getInitials(name); final initials = _getInitials(name);
return Padding( return Padding(
@@ -410,13 +432,13 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
Stack( Stack(
children: [ children: [
ClipOval( ClipOval(
child: avatarUrl.isNotEmpty child: (avatar != null && avatar.isNotEmpty)
? CachedNetworkImage( ? S3Image(
imageUrl: avatarUrl, imageUrl: avatar,
width: 80, width: 80,
height: 80, height: 80,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (_, e, s) => errorWidget: (_) =>
_AvatarFallback(letter: initials, size: 80), _AvatarFallback(letter: initials, size: 80),
) )
: _AvatarFallback(letter: initials, size: 80), : _AvatarFallback(letter: initials, size: 80),
@@ -440,8 +462,10 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
const SizedBox(height: 14), const SizedBox(height: 14),
// Name with pronouns and orange dot // Name with pronouns and orange dot
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Flexible(
child: Text(
name, name,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
@@ -449,8 +473,10 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(width: 4), ),
const SizedBox(width: 6),
const Text( const Text(
'(He/Him)', '(He/Him)',
style: TextStyle( style: TextStyle(
@@ -1041,17 +1067,16 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
// -- Message Avatar -- // -- Message Avatar --
Widget _buildMessageAvatar(String? avatarKey, String name, double size) { Widget _buildMessageAvatar(String? avatarKey, String name, double size) {
final avatarUrl = _resolveAvatarUrl(avatarKey);
final initials = _getInitials(name); final initials = _getInitials(name);
return ClipOval( return ClipOval(
child: avatarUrl.isNotEmpty child: (avatarKey != null && avatarKey.isNotEmpty)
? CachedNetworkImage( ? S3Image(
imageUrl: avatarUrl, imageUrl: avatarKey,
width: size, width: size,
height: size, height: size,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (_, e, s) => errorWidget: (_) =>
_AvatarFallback(letter: initials, size: size), _AvatarFallback(letter: initials, size: size),
) )
: _AvatarFallback(letter: initials, size: size), : _AvatarFallback(letter: initials, size: size),

View File

@@ -83,7 +83,13 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
children: [ children: [
_buildFilterChip('All', 'all'), _buildFilterChip('All', 'all'),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildFilterChip('Unread', 'unread'), Builder(builder: (context) {
final unreadCount = ref.watch(unreadCountProvider);
final label = unreadCount > 0
? 'Unread ($unreadCount)'
: 'Unread';
return _buildFilterChip(label, 'unread');
}),
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: () { onTap: () {