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:
pp-bot
2026-03-23 23:16:44 +09:00
parent d9539d391c
commit e715a0673b
37 changed files with 1226 additions and 405 deletions

View File

@@ -0,0 +1,42 @@
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/data/remote/fetch_history_request.dart';
import 'package:im_app/domain/repositories/message_repository.dart';
/// 拉取消息历史用例
///
/// 调用 HTTP GET `/app/api/chat/history`,将消息写入本地 DB。
/// DB Stream → StreamProvider → UI 自动重建。
///
/// ## 分页
///
/// [chatIdx] 为锚点消息 index
/// - 0 → 拉最新 [limit] 条
/// - N → 拉 idx < N 的 [limit] 条(向上翻页)
///
/// 返回拉取到的消息数0 表示已到头或网络失败。
class FetchHistoryUseCase {
final NetworksSdkApi _apiClient;
final MessageRepository _messageRepo;
FetchHistoryUseCase({
required NetworksSdkApi apiClient,
required MessageRepository messageRepo,
}) : _apiClient = apiClient,
_messageRepo = messageRepo;
Future<int> execute({
required int chatId,
int chatIdx = 0,
int limit = 20,
}) async {
final response = await _apiClient.executeRequest(
FetchHistoryRequest(chatId: chatId, chatIdx: chatIdx, limit: limit),
);
if (response == null || response.messages.isEmpty) return 0;
final entities = response.messages.map((m) => m.toEntity()).toList();
await _messageRepo.insertOrReplaceAll(entities);
return entities.length;
}
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/foundation.dart';
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/data/remote/send_message_request.dart';
import 'package:im_app/domain/entities/message.dart';
import 'package:im_app/domain/repositories/chat_repository.dart';
import 'package:im_app/domain/repositories/message_repository.dart';
/// 发送文本消息用例
///
/// ## 执行流程
/// 1. 乐观写入本地 DB临时消息id=0→ UI 立即刷新
/// 2. HTTP POST `/app/api/chat/send-message` → 获取服务端 messageId / chatIdx
/// 3. 更新 ChatRepository 的 lastMsg / lastTyp / lastTime
///
/// DB Stream → StreamProvider → UI 自动重建,无需额外通知。
class SendMessageUseCase {
final NetworksSdkApi _apiClient;
final MessageRepository _messageRepo;
final ChatRepository _chatRepo;
final int currentUid;
SendMessageUseCase({
required NetworksSdkApi apiClient,
required MessageRepository messageRepo,
required ChatRepository chatRepo,
required this.currentUid,
}) : _apiClient = apiClient,
_messageRepo = messageRepo,
_chatRepo = chatRepo;
Future<void> execute({
required int chatId,
required String content,
int typ = 1,
}) async {
final sendTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// 1. 乐观本地写入
await _messageRepo.insertOrReplace(
Message(
id: 0,
chatId: chatId,
sendId: currentUid,
content: content,
typ: typ,
sendTime: sendTime,
),
);
// 2. HTTP 发送
SendMessageResponse? resp;
try {
resp = await _apiClient.executeRequest(
SendMessageRequest(
chatId: chatId,
content: content,
typ: typ,
sendTime: sendTime,
),
);
} catch (e) {
debugPrint('[SendMessageUseCase] HTTP error: $e');
}
// 3. 更新 Chat 摘要
try {
final chat = await _chatRepo.getChat(chatId);
if (chat != null) {
await _chatRepo.updateChat(
chat.copyWith(
lastMsg: content,
lastTyp: typ,
lastTime: sendTime,
msgIdx: resp?.chatIdx ?? chat.msgIdx,
),
);
}
} catch (e) {
debugPrint('[SendMessageUseCase] updateChat error: $e');
}
}
}