import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:im_app/app/di/app_providers.dart'; import 'package:im_app/domain/entities/chat.dart'; import 'package:im_app/features/chat/di/chat_provider.dart'; import 'package:im_app/features/chat/di/chat_service_providers.dart'; import 'package:im_app/features/chat/presentation/chat_list_view_model.dart'; /// 聊天列表页 /// /// 从本地 DB Stream 读取会话列表,实时反映 WS 推送的新消息摘要。 /// 点击任意会话进入 [ChatDetailPage](chatId + 名称作为路由参数)。 class ChatPage extends ConsumerWidget { const ChatPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final vm = ref.watch(chatListViewModelProvider.notifier); final chatsAsync = ref.watch(allChatsProvider); // 监听输入状态变化 → 触发列表重建(对齐 iOS @Published typing 全局更新) ref.watch(typingChangeProvider); return Scaffold( appBar: AppBar( title: const Text('消息'), actions: [ IconButton( icon: const Icon(Icons.storage_outlined), tooltip: '数据库测试', onPressed: () => vm.goToDatabaseTest(context), ), ], ), body: chatsAsync.when( data: (chats) => chats.isEmpty ? const Center(child: Text('暂无会话')) : ListView.separated( itemCount: chats.length, separatorBuilder: (_, __) => const Divider(height: 1, indent: 72), itemBuilder: (context, index) => _ChatTile(chat: chats[index], vm: vm), ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('加载失败: $e')), ), ); } } class _ChatTile extends ConsumerWidget { const _ChatTile({required this.chat, required this.vm}); final Chat chat; final ChatListViewModel vm; @override Widget build(BuildContext context, WidgetRef ref) { final name = chat.name ?? 'Chat ${chat.chatId ?? chat.id}'; final lastMsg = chat.lastMsg ?? ''; final unread = chat.unreadNum ?? 0; final sendTime = chat.lastTime; final timeStr = sendTime != null ? _formatTime(sendTime) : ''; // 正在输入状态(对齐 iOS ConversationCell.typingText) final chatId = chat.chatId ?? chat.id; final myUid = ref.watch(authNotifierProvider).currentUid ?? 0; final typingMgr = ref.watch(typingIndicatorManagerProvider); final isGroup = (chat.typ ?? 1) == 2; final typingText = isGroup ? typingMgr.groupDisplayText(chatId: chatId, myUserId: myUid) : typingMgr.displayText(chatId: chatId, myUserId: myUid); return ListTile( leading: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primaryContainer, child: Text( name.isNotEmpty ? name[0].toUpperCase() : '?', style: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), title: Row( children: [ Expanded( child: Text(name, maxLines: 1, overflow: TextOverflow.ellipsis), ), if (timeStr.isNotEmpty) Text( timeStr, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.outline, ), ), ], ), subtitle: Row( children: [ Expanded( child: typingText != null // 输入状态优先显示(绿色,对齐 iOS onlineGreen) ? Text( typingText, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.green.shade600, ), ) : Text( lastMsg, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.outline, ), ), ), if (unread > 0) Container( margin: const EdgeInsets.only(left: 4), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Theme.of(context).colorScheme.error, borderRadius: BorderRadius.circular(10), ), child: Text( unread > 99 ? '99+' : '$unread', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onError, ), ), ), ], ), onTap: () => vm.openChat(context, chat), ); } String _formatTime(int unixSec) { final dt = DateTime.fromMillisecondsSinceEpoch(unixSec * 1000); final now = DateTime.now(); if (dt.year == now.year && dt.month == now.month && dt.day == now.day) { return '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}'; } return '${dt.month}/${dt.day}'; } }