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:
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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: () {
|
||||||
|
|||||||
Reference in New Issue
Block a user