feat(chat): 发收消息全量实现 (#25~#28)
- 移除 @riverpod/@freezed 注解依赖,全部改为手写 Provider(无需 build_runner) · LoginState 改为纯 Dart,LoginViewModel/ThemeViewModel/ChatViewModel 改为 Notifier · UserNotifier 改为 FamilyAsyncNotifier<User?,int>,mini_app_provider 改为手写 Provider · 15 个 StreamProvider/StreamProvider.family 从 @riverpod 迁移至手写 - 发送消息(#25) · SendMessageRequest/SendMessageResponse DTO · SendMessageUseCase:乐观写入 DB → HTTP POST → 更新 Chat 摘要 - 接收消息 WS(#26) · WsMessageService:监听 mode2 WS 帧 → HTTP 补拉 → DB 写入 → Chat 更新 · FetchHistoryRequest/FetchHistoryResponse DTO(GET /app/api/chat/history) · FetchHistoryUseCase:拉取 → insertOrReplaceAll - DI 装配(chat_service_providers.dart) · wsMessageServiceProvider、sendMessageUseCaseProvider、fetchHistoryUseCaseProvider - 聊天列表页(#27) · ChatListViewModel(Notifier<void>)+ chat_page.dart 真实会话列表 UI · ListTile:头像首字母、最新消息摘要、未读角标、时间格式化 - 聊天详情页(#28) · ChatDetailViewModel(FamilyNotifier<ChatDetailState,int>)+ chat_detail_page.dart · 消息气泡(自己/他人分左右)、底部输入框、发送状态与错误提示 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:im_app/app/di/user_provider.dart';
|
||||
import 'package:im_app/domain/entities/user.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'chat_db_test_view_model.g.dart';
|
||||
|
||||
class ChatDbTestState {
|
||||
final bool testStarted;
|
||||
@@ -41,8 +39,7 @@ class ChatDbTestState {
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
class ChatDbTestViewModel extends Notifier<ChatDbTestState> {
|
||||
final _random = Random();
|
||||
bool _isTesting = false;
|
||||
static const _pageSize = 50;
|
||||
@@ -81,7 +78,6 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
);
|
||||
}
|
||||
|
||||
/// Called by ListView when reaching the end
|
||||
void loadMore() {
|
||||
if (!state.hasMore || state.testStarted) return;
|
||||
_loadNextPage();
|
||||
@@ -211,3 +207,8 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final chatDbTestViewModelProvider =
|
||||
NotifierProvider<ChatDbTestViewModel, ChatDbTestState>(
|
||||
ChatDbTestViewModel.new,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:im_app/features/chat/di/chat_service_providers.dart';
|
||||
|
||||
/// 聊天详情页 ViewModel — 每个 chatId 独立实例
|
||||
///
|
||||
/// 状态包含:
|
||||
/// - [isSending] — 发送按钮 loading 状态
|
||||
/// - [error] — 最近一次发送失败的错误信息
|
||||
///
|
||||
/// 消息列表由 View 层直接 watch [messagesByChatIdProvider],不经过 ViewModel。
|
||||
///
|
||||
/// 首次进入时自动通过 [FetchHistoryUseCase] 拉取历史消息。
|
||||
class ChatDetailState {
|
||||
final bool isSending;
|
||||
final String? error;
|
||||
|
||||
const ChatDetailState({this.isSending = false, this.error});
|
||||
|
||||
ChatDetailState copyWith({bool? isSending, String? error}) =>
|
||||
ChatDetailState(
|
||||
isSending: isSending ?? this.isSending,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
class ChatDetailViewModel
|
||||
extends FamilyNotifier<ChatDetailState, int> {
|
||||
@override
|
||||
ChatDetailState build(int arg) {
|
||||
// 进入聊天时自动拉取历史
|
||||
Future.microtask(() => _fetchInitialHistory());
|
||||
return const ChatDetailState();
|
||||
}
|
||||
|
||||
// ── 操作 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> sendMessage(String content) async {
|
||||
final trimmed = content.trim();
|
||||
if (trimmed.isEmpty) return;
|
||||
|
||||
state = state.copyWith(isSending: true);
|
||||
try {
|
||||
await ref.read(sendMessageUseCaseProvider).execute(
|
||||
chatId: arg,
|
||||
content: trimmed,
|
||||
);
|
||||
state = const ChatDetailState();
|
||||
} catch (e) {
|
||||
state = state.copyWith(isSending: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchInitialHistory() async {
|
||||
try {
|
||||
await ref
|
||||
.read(fetchHistoryUseCaseProvider)
|
||||
.execute(chatId: arg, chatIdx: 0, limit: 30);
|
||||
} catch (_) {
|
||||
// 静默失败,UI 通过 DB Stream 反映现有数据
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final chatDetailViewModelProvider =
|
||||
NotifierProvider.family<ChatDetailViewModel, ChatDetailState, int>(
|
||||
ChatDetailViewModel.new,
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:im_app/app/router/app_route_name.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_service_providers.dart';
|
||||
|
||||
/// 聊天列表页 ViewModel
|
||||
///
|
||||
/// 负责:
|
||||
/// - 从 DB Stream 读取会话列表(通过 [allChatsProvider])
|
||||
/// - 导航到会话详情页
|
||||
/// - 初始化 WsMessageService(通过 [wsMessageServiceProvider])
|
||||
class ChatListViewModel extends Notifier<void> {
|
||||
@override
|
||||
void build() {
|
||||
// 确保 WS 消息服务已启动
|
||||
ref.read(wsMessageServiceProvider);
|
||||
}
|
||||
|
||||
void openChat(BuildContext context, Chat chat) {
|
||||
final chatId = chat.chatId ?? chat.id;
|
||||
final title = chat.name ?? 'Chat $chatId';
|
||||
context.push(
|
||||
AppRouteName.chatDetail.path,
|
||||
extra: (conversationId: chatId.toString(), title: title),
|
||||
);
|
||||
}
|
||||
|
||||
void goToDatabaseTest(BuildContext context) {
|
||||
context.push(AppRouteName.chatDBTest.path);
|
||||
}
|
||||
|
||||
void logout() {
|
||||
ref.read(authNotifierProvider).logout();
|
||||
}
|
||||
}
|
||||
|
||||
final chatListViewModelProvider =
|
||||
NotifierProvider<ChatListViewModel, void>(ChatListViewModel.new);
|
||||
@@ -1,38 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:im_app/app/di/app_providers.dart';
|
||||
import 'package:im_app/app/router/app_route_name.dart';
|
||||
|
||||
part 'chat_view_model.g.dart';
|
||||
|
||||
/// 聊天页 ViewModel(@riverpod 自动生成 `chatViewModelProvider`)
|
||||
///
|
||||
/// 当前 chat 页面为 Demo,无需从服务端加载数据,状态为 void。
|
||||
/// 后续接入会话列表时,将 build() 改为返回会话列表状态,并在此加载数据。
|
||||
///
|
||||
/// ## 数据流
|
||||
///
|
||||
/// ```
|
||||
/// ChatPage
|
||||
/// → ref.read(chatViewModelProvider.notifier).someMethod(context)
|
||||
/// → ★ ChatViewModel ★ ← 你在这里
|
||||
/// → 导航 / 业务逻辑
|
||||
/// ```
|
||||
@riverpod
|
||||
class ChatViewModel extends _$ChatViewModel {
|
||||
/// 聊天页 ViewModel(手动 NotifierProvider)
|
||||
class ChatViewModel extends Notifier<void> {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
// ── 导航(Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
|
||||
|
||||
/// 切换到联系人 Tab。
|
||||
void goToContact(BuildContext context) {
|
||||
context.go(AppRouteName.contact.path);
|
||||
}
|
||||
|
||||
/// 带 extra 参数 push 聊天详情页(extra 传 Dart Record)。
|
||||
void pushChatDetailWithExtra(BuildContext context) {
|
||||
context.push(
|
||||
AppRouteName.chatDetail.path,
|
||||
@@ -40,33 +21,26 @@ class ChatViewModel extends _$ChatViewModel {
|
||||
);
|
||||
}
|
||||
|
||||
/// 带路径参数 push 聊天详情页(id 内嵌在 URL 中)。
|
||||
void pushChatDetailById(BuildContext context) {
|
||||
context.push(AppRouteName.chatDetailByIdPath('99'));
|
||||
}
|
||||
|
||||
/// 无参 push(演示 push 导航)。
|
||||
void pushSettingsTheme(BuildContext context) {
|
||||
context.push(AppRouteName.settingsTheme.path);
|
||||
}
|
||||
|
||||
/// 切换到设置 Tab。
|
||||
void goToSettings(BuildContext context) {
|
||||
context.go(AppRouteName.settings.path);
|
||||
}
|
||||
|
||||
/// 测试数据库性能
|
||||
void goToDatabaseTest(BuildContext context) {
|
||||
context.push(AppRouteName.chatDBTest.path);
|
||||
}
|
||||
|
||||
// ── 业务 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 退出登录
|
||||
///
|
||||
/// 调用 [AuthNotifier.logout] 清除登录状态,go_router 守卫检测到后
|
||||
/// 自动重定向到登录页,无需手动导航。
|
||||
void logout() {
|
||||
ref.read(authNotifierProvider).logout();
|
||||
}
|
||||
}
|
||||
|
||||
final chatViewModelProvider =
|
||||
NotifierProvider<ChatViewModel, void>(ChatViewModel.new);
|
||||
|
||||
Reference in New Issue
Block a user