refactor: improve specialization card layout and unify chat screen header and navigation with home screen components.

This commit is contained in:
pradeepkumar
2026-03-08 02:01:03 +05:30
parent bba9cd3936
commit 219acfc013
14 changed files with 2498 additions and 236 deletions

View File

@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/features/auth/presentation/providers/auth_provider.dart';
@@ -91,7 +92,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
void _handleBack() {
ref.read(messagingProvider.notifier).setActiveConversation(null);
Navigator.pop(context);
context.go('/messages');
}
// ============================================================
@@ -183,51 +184,37 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
final otherPartyName = conversation?.otherParty.name ?? 'Chat';
final otherPartyAvatar = conversation?.otherParty.avatar;
final isOnline = conversation?.otherParty.isOnline ?? false;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
_buildHeader(
name: otherPartyName,
isOnline: isOnline,
),
Expanded(
child: messagingState.isLoadingMessages && messages.isEmpty
? const Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
strokeWidth: 2,
),
)
: messages.isEmpty
? Center(
child: Text(
'No messages yet.\nSay hello!',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.hintText,
),
),
)
: _buildMessagesList(
messages: messages,
currentUserId: currentUserId ?? '',
currentUserName: 'You',
otherPartyName: otherPartyName,
otherPartyAvatar: otherPartyAvatar,
isTyping: isTyping,
isLoadingMore: messagingState.isLoadingMessages &&
messages.isNotEmpty,
),
),
_buildInputArea(),
],
final otherPartyHeadline = conversation?.otherParty.headline;
return Column(
children: [
// Chat header with name + status
_buildHeader(
name: otherPartyName,
isOnline: isOnline,
),
),
Expanded(
child: messagingState.isLoadingMessages && messages.isEmpty
? const Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
strokeWidth: 2,
),
)
: _buildChatBody(
messages: messages,
currentUserId: currentUserId ?? '',
otherPartyName: otherPartyName,
otherPartyAvatar: otherPartyAvatar,
otherPartyHeadline: otherPartyHeadline,
isOnline: isOnline,
isTyping: isTyping,
isLoadingMore: messagingState.isLoadingMessages &&
messages.isNotEmpty,
),
),
_buildInputArea(),
],
);
}
@@ -240,104 +227,277 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
required bool isOnline,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Container(
height: 55,
decoration: BoxDecoration(
border: Border.all(
color: AppColors.primaryDark,
width: 0.5,
),
borderRadius: BorderRadius.circular(7),
),
child: Row(
children: [
// Back arrow
GestureDetector(
onTap: _handleBack,
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 14),
child: Icon(
Icons.arrow_back,
size: 23,
color: AppColors.primaryDark,
),
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
child: Row(
children: [
// Back arrow
GestureDetector(
onTap: _handleBack,
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(
Icons.arrow_back,
size: 22,
color: AppColors.primaryDark,
),
),
// Name and active status
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// Name and active status
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
const SizedBox(height: 2),
Row(
children: [
if (isOnline) ...[
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF4CAF50),
shape: BoxShape.circle,
),
),
const SizedBox(width: 5),
],
Text(
isOnline ? 'Active Now' : 'Offline',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 10,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Row(
children: [
if (isOnline) ...[
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF4CAF50),
shape: BoxShape.circle,
),
),
const SizedBox(width: 5),
],
),
],
Text(
isOnline ? 'Active Now' : 'Offline',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 10,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
],
),
),
// 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 (vertical)
GestureDetector(
onTap: () {},
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Icon(
Icons.more_vert,
size: 20,
color: AppColors.primaryDark,
),
),
// 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,
),
),
// Star icon
GestureDetector(
onTap: () {},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(right: 14),
child: Icon(
Icons.star_rounded,
size: 24,
color: AppColors.accentOrange,
),
),
],
),
);
}
// ============================================================
// CHAT BODY (profile section + messages)
// ============================================================
Widget _buildChatBody({
required List<ChatMessage> messages,
required String currentUserId,
required String otherPartyName,
String? otherPartyAvatar,
String? otherPartyHeadline,
required bool isOnline,
required bool isTyping,
required bool isLoadingMore,
}) {
if (messages.isEmpty) {
return SingleChildScrollView(
child: Column(
children: [
_buildProfileSection(
name: otherPartyName,
avatar: otherPartyAvatar,
headline: otherPartyHeadline,
isOnline: isOnline,
),
const SizedBox(height: 60),
Text(
'No messages yet.\nSay hello!',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.hintText,
),
),
],
),
);
}
return _buildMessagesList(
messages: messages,
currentUserId: currentUserId,
currentUserName: 'You',
otherPartyName: otherPartyName,
otherPartyAvatar: otherPartyAvatar,
otherPartyHeadline: otherPartyHeadline,
isOnline: isOnline,
isTyping: isTyping,
isLoadingMore: isLoadingMore,
);
}
// ============================================================
// PROFILE SECTION (large avatar, name, specialties)
// ============================================================
Widget _buildProfileSection({
required String name,
String? avatar,
String? headline,
required bool isOnline,
}) {
final avatarUrl = _resolveAvatarUrl(avatar);
final initials = _getInitials(name);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Large avatar with online dot
Stack(
children: [
ClipOval(
child: avatarUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: avatarUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
errorWidget: (_, __, ___) =>
_AvatarFallback(letter: initials, size: 80),
)
: _AvatarFallback(letter: initials, size: 80),
),
if (isOnline)
Positioned(
right: 2,
bottom: 2,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2.5),
),
),
),
],
),
const SizedBox(height: 14),
// Name with pronouns and orange dot
Row(
children: [
Text(
name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 4),
const Text(
'(He/Him)',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 6),
Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: AppColors.accentOrange,
shape: BoxShape.circle,
),
),
],
),
const SizedBox(height: 12),
// Specialties row
if (headline != null && headline.isNotEmpty)
_buildSpecialtiesRow(headline)
else
_buildSpecialtiesRow('Sales,Analytics,Inspection,Residential,Commercial'),
const SizedBox(height: 12),
// Divider
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.1),
thickness: 0.5,
),
],
),
);
}
Widget _buildSpecialtiesRow(String headline) {
// Split by comma, pipe, or similar separators
final specialties = headline.split(RegExp(r'[,|;]'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
if (specialties.isEmpty) return const SizedBox.shrink();
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (int i = 0; i < specialties.length; i++) ...[
Text(
specialties[i],
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
if (i < specialties.length - 1)
const SizedBox(width: 16),
],
],
),
);
}
@@ -352,16 +512,19 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
required String currentUserName,
required String otherPartyName,
String? otherPartyAvatar,
String? otherPartyHeadline,
required bool isOnline,
required bool isTyping,
required bool isLoadingMore,
}) {
// +1 for the profile section header at the end of reversed list
final itemCount =
messages.length + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0);
messages.length + 1 + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0);
return ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
itemCount: itemCount,
itemBuilder: (context, index) {
if (isTyping && index == 0) {
@@ -370,6 +533,17 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
final adjustedIndex = isTyping ? index - 1 : index;
// Profile section at the top (last index in reversed list)
final profileIndex = messages.length + (isLoadingMore ? 1 : 0);
if (adjustedIndex == profileIndex) {
return _buildProfileSection(
name: otherPartyName,
avatar: otherPartyAvatar,
headline: otherPartyHeadline,
isOnline: isOnline,
);
}
if (isLoadingMore && adjustedIndex == messages.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
@@ -404,19 +578,22 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
: otherPartyName;
final senderAvatar = isMine ? null : otherPartyAvatar;
return Column(
children: [
if (showDateSeparator)
_buildDateSeparator(
DateTime.parse(message.createdAt).toLocal(),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
if (showDateSeparator)
_buildDateSeparator(
DateTime.parse(message.createdAt).toLocal(),
),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: isMine
? _buildSentMessage(message, senderName)
: _buildReceivedMessage(message, senderName, senderAvatar),
),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: isMine
? _buildSentMessage(message, senderName)
: _buildReceivedMessage(message, senderName, senderAvatar),
),
],
],
),
);
},
);
@@ -495,6 +672,16 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
),
),
const SizedBox(width: 6),
const Text(
'He/Him',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 10,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 6),
Container(
width: 8,
height: 8,
@@ -567,6 +754,16 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
),
),
const SizedBox(width: 6),
const Text(
'He/Him',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 10,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 6),
Text(
timestamp,
style: const TextStyle(
@@ -667,41 +864,30 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Plus icon in circle border
// Plus icon (plain, no border)
GestureDetector(
onTap: _showAttachmentOptions,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.primaryDark,
width: 0.5,
),
),
child: const Center(
child: Icon(
Icons.add,
size: 24,
color: AppColors.primaryDark,
),
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.only(right: 10),
child: Icon(
Icons.add,
size: 24,
color: AppColors.primaryDark,
),
),
),
const SizedBox(width: 10),
// Text input
// Text input pill with send icon inside
Expanded(
child: Container(
height: 38,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.primaryDark,
width: 0.5,
width: 1.5,
),
),
child: Row(
@@ -736,7 +922,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
),
),
),
// Send icon inside the input field
// Send arrow inside the pill
GestureDetector(
onTap: _sendMessage,
behavior: HitTestBehavior.opaque,
@@ -755,25 +941,21 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
),
const SizedBox(width: 10),
// Mic icon in circle border
// Mic icon in filled dark circle
GestureDetector(
onTap: () => _showComingSoon('Speech to text'),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
decoration: const BoxDecoration(
color: AppColors.primaryDark,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.primaryDark,
width: 0.5,
),
),
child: const Center(
child: Icon(
Icons.mic_none,
size: 20,
color: AppColors.primaryDark,
color: Colors.white,
),
),
),
@@ -873,6 +1055,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
);
}
}
// -- Avatar Fallback --