update favorite, chat
This commit is contained in:
462
lib/features/chat/presentation/pages/chat_list_page.dart
Normal file
462
lib/features/chat/presentation/pages/chat_list_page.dart
Normal file
@@ -0,0 +1,462 @@
|
||||
/// Page: Chat List Page
|
||||
///
|
||||
/// List of chat conversations following html/chat-list.html design.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Chat List Page
|
||||
///
|
||||
/// Shows list of chat conversations with:
|
||||
/// - Search functionality
|
||||
/// - Conversation items with avatars
|
||||
/// - Unread indicators
|
||||
/// - Online status
|
||||
/// - Load more button
|
||||
class ChatListPage extends ConsumerStatefulWidget {
|
||||
const ChatListPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ChatListPage> createState() => _ChatListPageState();
|
||||
}
|
||||
|
||||
class _ChatListPageState extends ConsumerState<ChatListPage> {
|
||||
bool _isSearchVisible = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleSearch() {
|
||||
setState(() {
|
||||
_isSearchVisible = !_isSearchVisible;
|
||||
if (!_isSearchVisible) {
|
||||
_searchController.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _newConversation() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Chức năng tạo cuộc trò chuyện mới sẽ được triển khai trong phiên bản tiếp theo.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openChat(String conversationId) {
|
||||
// TODO: Navigate to chat detail page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Mở cuộc trò chuyện: $conversationId')),
|
||||
);
|
||||
}
|
||||
|
||||
void _loadMore() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Đang tải thêm cuộc trò chuyện...')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grey50,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
centerTitle: false,
|
||||
title: const Text(
|
||||
'Tin nhắn',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isSearchVisible ? Icons.search : Icons.search,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: _toggleSearch,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: Colors.black),
|
||||
onPressed: _newConversation,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search Bar (conditional)
|
||||
if (_isSearchVisible)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
color: AppColors.white,
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Tìm kiếm cuộc trò chuyện...',
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.grey500),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, color: AppColors.grey500),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: AppColors.grey50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Conversations List
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
// Conversation 1 - Order Reference
|
||||
_ConversationItem(
|
||||
avatarIcon: Icons.inventory_2,
|
||||
avatarGradient: const [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
contactName: 'Đơn hàng #SO001234',
|
||||
messageTime: '14:30',
|
||||
lastMessage: 'Đơn hàng đang được giao - Dự kiến đến 16:00',
|
||||
lastMessageIcon: Icons.local_shipping,
|
||||
contactType: 'Về: Đơn hàng #SO001234',
|
||||
lastSeen: 'Cập nhật mới',
|
||||
isUnread: true,
|
||||
unreadCount: 2,
|
||||
isOnline: true,
|
||||
onTap: () => _openChat('order001'),
|
||||
),
|
||||
|
||||
// Conversation 2 - Product Reference
|
||||
_ConversationItem(
|
||||
avatarIcon: Icons.inventory_outlined,
|
||||
avatarGradient: null,
|
||||
avatarBackgroundColor: AppColors.grey50,
|
||||
iconColor: AppColors.primaryBlue,
|
||||
contactName: 'Sản phẩm PR0123',
|
||||
messageTime: '12:20',
|
||||
lastMessage: 'Thông tin bổ sung về gạch Granite 60x60',
|
||||
lastMessageIcon: Icons.info_outline,
|
||||
contactType: 'Đơn hàng #DH001233',
|
||||
lastSeen: '2 giờ trước',
|
||||
isUnread: true,
|
||||
unreadCount: 1,
|
||||
isOnline: false,
|
||||
isAway: true,
|
||||
onTap: () => _openChat('product001'),
|
||||
),
|
||||
|
||||
// Conversation 3 - Support Team
|
||||
_ConversationItem(
|
||||
avatarIcon: Icons.headset_mic,
|
||||
avatarGradient: const [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
contactName: 'Tổng đài hỗ trợ',
|
||||
messageTime: '13:45',
|
||||
lastMessage: 'Thông tin về quy trình đổi trả sản phẩm',
|
||||
contactType: 'Bộ phận hỗ trợ',
|
||||
lastSeen: 'Đang hoạt động',
|
||||
isUnread: false,
|
||||
isOnline: true,
|
||||
onTap: () => _openChat('support001'),
|
||||
),
|
||||
|
||||
// Load More Button
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: AppColors.white,
|
||||
child: Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _loadMore,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(color: AppColors.grey100),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.expand_more, size: 20),
|
||||
label: const Text(
|
||||
'Tải thêm cuộc trò chuyện',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversation Item Widget
|
||||
class _ConversationItem extends StatelessWidget {
|
||||
const _ConversationItem({
|
||||
required this.avatarIcon,
|
||||
this.avatarGradient,
|
||||
this.avatarBackgroundColor,
|
||||
this.iconColor,
|
||||
required this.contactName,
|
||||
required this.messageTime,
|
||||
required this.lastMessage,
|
||||
this.lastMessageIcon,
|
||||
required this.contactType,
|
||||
required this.lastSeen,
|
||||
this.isUnread = false,
|
||||
this.unreadCount,
|
||||
this.isOnline = false,
|
||||
this.isAway = false,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData avatarIcon;
|
||||
final List<Color>? avatarGradient;
|
||||
final Color? avatarBackgroundColor;
|
||||
final Color? iconColor;
|
||||
final String contactName;
|
||||
final String messageTime;
|
||||
final String lastMessage;
|
||||
final IconData? lastMessageIcon;
|
||||
final String contactType;
|
||||
final String lastSeen;
|
||||
final bool isUnread;
|
||||
final int? unreadCount;
|
||||
final bool isOnline;
|
||||
final bool isAway;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isUnread
|
||||
? const Color(0xFFF0F9FF) // Light blue background for unread
|
||||
: AppColors.white,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: AppColors.grey100, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Avatar with online indicator
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
gradient: avatarGradient != null
|
||||
? LinearGradient(
|
||||
colors: avatarGradient!,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)
|
||||
: null,
|
||||
color: avatarBackgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
avatarIcon,
|
||||
color: iconColor ?? AppColors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
// Online indicator
|
||||
Positioned(
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: isOnline
|
||||
? AppColors.success
|
||||
: isAway
|
||||
? AppColors.warning
|
||||
: AppColors.grey500,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.white, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Conversation content
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header: Contact name and time
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
contactName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
messageTime,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Preview: Last message and unread count
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (lastMessageIcon != null) ...[
|
||||
Icon(
|
||||
lastMessageIcon,
|
||||
size: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
lastMessage,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (unreadCount != null && unreadCount! > 0) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
child: Text(
|
||||
'$unreadCount',
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Meta: Contact type and last seen
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
contactType,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Text(
|
||||
'•',
|
||||
style: TextStyle(
|
||||
color: AppColors.grey100,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
lastSeen,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user