Files
mobile-app/lib/features/notifications/presentation/screens/notifications_screen.dart

440 lines
14 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/notifications/data/models/notification_model.dart';
import 'package:real_estate_mobile/features/notifications/presentation/providers/notification_provider.dart';
class NotificationsScreen extends ConsumerStatefulWidget {
const NotificationsScreen({super.key});
@override
ConsumerState<NotificationsScreen> createState() =>
_NotificationsScreenState();
}
class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
final ScrollController _scrollController = ScrollController();
String _activeFilter = 'all';
@override
void initState() {
super.initState();
Future.microtask(() {
ref.read(notificationListProvider.notifier).loadNotifications();
});
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
ref.read(notificationListProvider.notifier).loadMore();
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final state = ref.watch(notificationListProvider);
return Column(
children: [
// ── Header bar matching Figma: "Notifications" with bell icon ──
Container(
height: 55,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 0.1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.notifications,
color: AppColors.accentOrange,
size: 22,
),
const SizedBox(width: 8),
const Text(
'Notifications',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
],
),
),
// ── Filter tabs + Mark all as read ──
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_buildFilterChip('All', 'all'),
const SizedBox(width: 8),
Builder(builder: (context) {
final unreadCount = ref.watch(unreadCountProvider);
final label = unreadCount > 0
? 'Unread ($unreadCount)'
: 'Unread';
return _buildFilterChip(label, 'unread');
}),
const Spacer(),
GestureDetector(
onTap: () {
ref
.read(notificationListProvider.notifier)
.markAllAsRead();
},
child: const Text(
'Mark all as read',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
fontWeight: FontWeight.w400,
color: AppColors.accentOrange,
),
),
),
],
),
),
// ── Notification list ──
Expanded(
child: _buildContent(state),
),
],
);
}
Widget _buildFilterChip(String label, String filter) {
final isActive = _activeFilter == filter;
return GestureDetector(
onTap: () {
setState(() => _activeFilter = filter);
ref
.read(notificationListProvider.notifier)
.loadNotifications(filter: filter);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: isActive ? AppColors.primaryDark : Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isActive
? AppColors.primaryDark
: AppColors.primaryDark.withValues(alpha: 0.2),
),
),
child: Text(
label,
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 13,
fontWeight: FontWeight.w500,
color: isActive ? Colors.white : AppColors.primaryDark,
),
),
),
);
}
Widget _buildContent(NotificationListState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
strokeWidth: 2,
),
);
}
if (state.error != null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline,
size: 48, color: AppColors.accentOrange),
const SizedBox(height: 16),
Text(
'Failed to load notifications',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () {
ref
.read(notificationListProvider.notifier)
.loadNotifications(filter: _activeFilter);
},
child: const Text('Retry',
style: TextStyle(color: AppColors.accentOrange)),
),
],
),
);
}
if (state.notifications.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.notifications_off_outlined,
size: 48,
color: AppColors.primaryDark.withValues(alpha: 0.3)),
const SizedBox(height: 16),
Text(
_activeFilter == 'unread'
? 'No unread notifications'
: 'No notifications yet',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
),
);
}
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.zero,
itemCount: state.notifications.length + (state.isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.notifications.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
strokeWidth: 2,
),
),
);
}
return _buildNotificationTile(state.notifications[index]);
},
);
}
Widget _buildNotificationTile(AppNotification notification) {
return GestureDetector(
onTap: () {
if (!notification.read) {
ref
.read(notificationListProvider.notifier)
.markAsRead(notification.id);
}
_navigateToNotification(notification);
},
behavior: HitTestBehavior.opaque,
child: Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Avatar with unread dot ──
SizedBox(
width: 53,
height: 52,
child: Stack(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _iconBgColor(notification.type),
),
child: Center(
child: Icon(
_iconForType(notification.type),
color: AppColors.primaryDark,
size: 24,
),
),
),
if (!notification.read)
Positioned(
right: 0,
bottom: 4,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.accentOrange,
),
),
),
],
),
),
const SizedBox(width: 8),
// ── Title + Description ──
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
notification.title,
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight:
notification.read ? FontWeight.w500 : FontWeight.w700,
color: AppColors.primaryDark,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
notification.description,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark
.withValues(alpha: notification.read ? 0.5 : 0.8),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
// ── Time ago ──
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
_timeAgo(notification.createdAt),
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color:
AppColors.primaryDark.withValues(alpha: 0.6),
),
),
),
],
),
),
Divider(
height: 0.1,
thickness: 0.1,
color: AppColors.primaryDark.withValues(alpha: 0.1),
),
],
),
);
}
void _navigateToNotification(AppNotification notification) {
final data = notification.data;
final type = data?['type'] ?? notification.type;
switch (type) {
case 'message':
final conversationId = data?['conversationId'];
if (conversationId != null) {
context.push('/messages/chat/$conversationId');
} else {
context.go('/messages');
}
break;
case 'connection_request':
case 'connection':
context.go('/agent/network');
break;
case 'connection_response':
final status = data?['status'];
if (status == 'ACCEPTED') {
context.go('/messages');
} else {
context.go('/home');
}
break;
default:
break;
}
}
IconData _iconForType(String type) {
switch (type) {
case 'connection':
return Icons.person_outline;
case 'message':
return Icons.chat_bubble_outline;
case 'request':
return Icons.assignment_outlined;
case 'update':
return Icons.notifications_outlined;
case 'system':
return Icons.settings_outlined;
default:
return Icons.notifications_outlined;
}
}
Color _iconBgColor(String type) {
switch (type) {
case 'connection':
return const Color(0xFFE8F5E9);
case 'message':
return const Color(0xFFE3F2FD);
case 'request':
return const Color(0xFFFFF3E0);
case 'update':
return const Color(0xFFF3E5F5);
case 'system':
return const Color(0xFFF5F5F5);
default:
return const Color(0xFFF5F5F5);
}
}
String _timeAgo(DateTime dateTime) {
final now = DateTime.now();
final diff = now.difference(dateTime);
if (diff.inSeconds < 60) return 'Just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}hr ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
if (diff.inDays < 30) return '${(diff.inDays / 7).floor()}w ago';
return '${(diff.inDays / 30).floor()}mo ago';
}
}