feat: Enhance socket reconnection and chat screen lifecycle management, and refine agent detail and home screen UI.
This commit is contained in:
@@ -656,7 +656,11 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Location (wrapping) + Member Since (separate line)
|
// Location (wrapping) + Member Since (separate line)
|
||||||
Builder(builder: (_) {
|
Builder(builder: (_) {
|
||||||
final parts = state.locationParts;
|
var parts = state.locationParts;
|
||||||
|
// Fallback: use agent model location if fieldValues-based parts are empty
|
||||||
|
if (parts.isEmpty && agent.location.isNotEmpty) {
|
||||||
|
parts = agent.location.split(', ').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||||
|
}
|
||||||
const maxVisible = 4;
|
const maxVisible = 4;
|
||||||
final visibleParts = parts.take(maxVisible).join(', ');
|
final visibleParts = parts.take(maxVisible).join(', ');
|
||||||
final remaining = parts.length - maxVisible;
|
final remaining = parts.length - maxVisible;
|
||||||
@@ -1520,8 +1524,9 @@ class _SpecializationCardWidgetState extends State<_SpecializationCardWidget> {
|
|||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 298,
|
width: 298,
|
||||||
constraints: const BoxConstraints(minHeight: 236),
|
height: _expanded ? null : 236,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
@@ -1531,6 +1536,7 @@ class _SpecializationCardWidgetState extends State<_SpecializationCardWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: _expanded ? MainAxisSize.min : MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
Icon(_icon, size: 32, color: AppColors.accentOrange),
|
Icon(_icon, size: 32, color: AppColors.accentOrange),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -1083,20 +1083,22 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
final displayName = AgentDetailState.titleCase(card.name);
|
final displayName = AgentDetailState.titleCase(card.name);
|
||||||
final sectionKey = 'spec_${card.slug}';
|
final sectionKey = 'spec_${card.slug}';
|
||||||
final isExpanded = _expandedTagSections.contains(sectionKey);
|
final isExpanded = _expandedTagSections.contains(sectionKey);
|
||||||
final displayValues = isExpanded ? card.values : card.values.take(5).toList();
|
final displayValues = isExpanded ? card.values : card.values.take(3).toList();
|
||||||
final hasMore = card.values.length > 5;
|
final hasMore = card.values.length > 3;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 66, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 66, vertical: 8),
|
||||||
width: 298,
|
width: 298,
|
||||||
constraints: const BoxConstraints(minHeight: 236),
|
height: isExpanded ? null : 236,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
border: Border.all(color: AppColors.primaryDark, width: 0.7),
|
border: Border.all(color: AppColors.primaryDark, width: 0.7),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: isExpanded ? MainAxisSize.min : MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.category_outlined,
|
const Icon(Icons.category_outlined,
|
||||||
size: 28, color: AppColors.accentOrange),
|
size: 28, color: AppColors.accentOrange),
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
// Testimonials Section
|
// Testimonials Section
|
||||||
const TestimonialsSection(),
|
const TestimonialsSection(),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
// Footer
|
|
||||||
_buildFooter(context),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -59,7 +56,7 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
'FAQ': '/faq',
|
'FAQ': '/faq',
|
||||||
};
|
};
|
||||||
|
|
||||||
Widget _buildFooter(BuildContext context) {
|
Widget _buildFooter(BuildContext context, WidgetRef ref) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||||
@@ -76,13 +73,13 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildFooterLink(context, 'Home'),
|
_buildFooterLink(context, 'Home', ref),
|
||||||
_buildFooterDot(),
|
_buildFooterDot(),
|
||||||
_buildFooterLink(context, 'About'),
|
_buildFooterLink(context, 'About', ref),
|
||||||
_buildFooterDot(),
|
_buildFooterDot(),
|
||||||
_buildFooterLink(context, 'Contact'),
|
_buildFooterLink(context, 'Contact', ref),
|
||||||
_buildFooterDot(),
|
_buildFooterDot(),
|
||||||
_buildFooterLink(context, 'FAQ'),
|
_buildFooterLink(context, 'FAQ', ref),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -100,11 +97,22 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFooterLink(BuildContext context, String text) {
|
Widget _buildFooterLink(BuildContext context, String text, WidgetRef ref) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final route = _footerRoutes[text];
|
final route = _footerRoutes[text];
|
||||||
if (route != null) context.go(route);
|
if (route == null) return;
|
||||||
|
final currentLocation = GoRouterState.of(context).uri.toString();
|
||||||
|
if (route == '/home' && currentLocation == '/home') {
|
||||||
|
// Already on home — scroll to top
|
||||||
|
ref.read(homeScrollControllerProvider).animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.go(route);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ class SocketService {
|
|||||||
.setAuth({'token': token})
|
.setAuth({'token': token})
|
||||||
.disableAutoConnect()
|
.disableAutoConnect()
|
||||||
.enableReconnection()
|
.enableReconnection()
|
||||||
.setReconnectionAttempts(5)
|
.setReconnectionAttempts(double.maxFinite.toInt())
|
||||||
.setReconnectionDelay(1000)
|
.setReconnectionDelay(1000)
|
||||||
.setReconnectionDelayMax(5000)
|
.setReconnectionDelayMax(10000)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,6 +97,18 @@ class SocketService {
|
|||||||
_connectionController.add(false);
|
_connectionController.add(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_socket!.onReconnect((_) {
|
||||||
|
_log.i('Socket reconnected successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
_socket!.onReconnectAttempt((attempt) {
|
||||||
|
_log.w('Socket reconnection attempt #$attempt');
|
||||||
|
});
|
||||||
|
|
||||||
|
_socket!.onReconnectError((error) {
|
||||||
|
_log.e('Socket reconnection error: $error');
|
||||||
|
});
|
||||||
|
|
||||||
// Listen for events
|
// Listen for events
|
||||||
_socket!.on('new_message', (data) {
|
_socket!.on('new_message', (data) {
|
||||||
try {
|
try {
|
||||||
@@ -172,6 +184,12 @@ class SocketService {
|
|||||||
_socket?.connect();
|
_socket?.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure socket is connected — reconnect if needed.
|
||||||
|
Future<void> ensureConnected() async {
|
||||||
|
if (_socket?.connected == true) return;
|
||||||
|
await connect();
|
||||||
|
}
|
||||||
|
|
||||||
void disconnect() {
|
void disconnect() {
|
||||||
_intentionalDisconnect = true;
|
_intentionalDisconnect = true;
|
||||||
_socket?.disconnect();
|
_socket?.disconnect();
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class ChatScreen extends ConsumerStatefulWidget {
|
|||||||
ConsumerState<ChatScreen> createState() => _ChatScreenState();
|
ConsumerState<ChatScreen> createState() => _ChatScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends ConsumerState<ChatScreen> {
|
class _ChatScreenState extends ConsumerState<ChatScreen>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final TextEditingController _messageController = TextEditingController();
|
final TextEditingController _messageController = TextEditingController();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
final FocusNode _messageFocusNode = FocusNode();
|
final FocusNode _messageFocusNode = FocusNode();
|
||||||
@@ -36,6 +37,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final notifier = ref.read(messagingProvider.notifier);
|
final notifier = ref.read(messagingProvider.notifier);
|
||||||
notifier.setActiveConversation(widget.conversationId);
|
notifier.setActiveConversation(widget.conversationId);
|
||||||
@@ -43,10 +45,12 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
notifier.markAsRead(widget.conversationId);
|
notifier.markAsRead(widget.conversationId);
|
||||||
// Suppress push notifications for this conversation while viewing
|
// Suppress push notifications for this conversation while viewing
|
||||||
PushNotificationService().activeConversationId = widget.conversationId;
|
PushNotificationService().activeConversationId = widget.conversationId;
|
||||||
// Join socket room for real-time updates
|
// Ensure socket is connected and join room for real-time updates
|
||||||
if (_socket.isConnected) {
|
_socket.ensureConnected().then((_) {
|
||||||
_socket.joinConversation(widget.conversationId);
|
if (_socket.isConnected) {
|
||||||
}
|
_socket.joinConversation(widget.conversationId);
|
||||||
|
}
|
||||||
|
});
|
||||||
// Listen for ALL connection changes (initial connect + reconnects)
|
// Listen for ALL connection changes (initial connect + reconnects)
|
||||||
// so we re-join the room after any reconnection
|
// so we re-join the room after any reconnection
|
||||||
_connectionSub = _socket.onConnectionChange.listen((connected) {
|
_connectionSub = _socket.onConnectionChange.listen((connected) {
|
||||||
@@ -59,8 +63,23 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
_messageController.addListener(_onTextChanged);
|
_messageController.addListener(_onTextChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
// App came back to foreground — reconnect socket and refresh messages
|
||||||
|
_socket.ensureConnected().then((_) {
|
||||||
|
if (_socket.isConnected && mounted) {
|
||||||
|
_socket.joinConversation(widget.conversationId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Refresh messages to catch anything missed while in background
|
||||||
|
ref.read(messagingProvider.notifier).loadMessages(widget.conversationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_connectionSub?.cancel();
|
_connectionSub?.cancel();
|
||||||
// Stop typing and leave room before disposing
|
// Stop typing and leave room before disposing
|
||||||
if (_isComposing) {
|
if (_isComposing) {
|
||||||
|
|||||||
Reference in New Issue
Block a user