feat(chat): 表情/贴纸/附件面板全量实现(#51~#56)

## 贴纸(#51)
- `StickerMessageBubble`:typ=5,CDN 图片,120×120pt max,无气泡背景,圆角 8pt
- `_buildBubble()` switch 新增 `case 5:`,isMedia 添加 typ=5

## 附件面板(#52~#55)
- `AttachmentPanelSheet`:6 格 BottomSheet(拍照/相册/视频/文件/录音/红包)
- 返回 `AttachmentOption` enum,由 `_ChatDetailPageState` 分发后续行为
- 拍照(#53):`image_picker.pickImage(camera)` → `SendImageUseCase`
- 视频(#54):`image_picker.pickVideo()` → `SendVideoUseCase`(新建)
- 文件/录音/红包:SnackBar 占位「暂未支持」
- `SendVideoUseCase`:上传 + `jsonEncode({url,thumb,size})` → typ=4

## 表情面板(#56)
- `EmojiPanel`:4 分类(常用/人物/自然/物件),每类 ~64 个 Unicode emoji
- 点击插入到 `TextEditingController` 当前光标位置(支持多码点 emoji)
- ⌫ 退格按钮按 rune 删除(正确处理多码点 emoji)
- `_InputBar` 新增表情按钮(😊)和附件按钮,原 `onAttach` 拆分为 `onAttachPanel` + `onEmoji`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
pp-bot
2026-03-24 21:06:24 +09:00
parent 8d5059add1
commit bb9f1aa956
6 changed files with 702 additions and 8 deletions

View File

@@ -0,0 +1,79 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/data/remote/upload_file_request.dart';
import 'package:im_app/features/chat/usecases/send_message_use_case.dart';
/// 视频上传并发送消息用例typ=4Gitea issue #54
///
/// 对应 iOS PhotosPicker 单视频选取 + UploadService + sendMessage(typ=4)
///
/// ## 流程
/// 1. 读取文件大小
/// 2. [UploadFileRequest] 上传视频文件到 CDN
/// 3. `jsonEncode({"url":url,"thumb":"","size":N})` → [SendMessageUseCase] typ=4
///
/// ## 说明
/// - 缩略图thumb当前为空字符串生成视频首帧需要 `video_thumbnail` 等额外包
/// - [VideoMessageBubble] 已有,无需改动
class SendVideoUseCase {
final NetworksSdkApi _apiClient;
final SendMessageUseCase _sendMessage;
SendVideoUseCase({
required NetworksSdkApi apiClient,
required SendMessageUseCase sendMessage,
}) : _apiClient = apiClient,
_sendMessage = sendMessage;
/// 上传并发送视频消息
///
/// [filePath]:本地视频文件路径(由 `image_picker.pickVideo()` 返回的 XFile.path
/// [chatId]:目标会话 ID
Future<void> execute({
required String filePath,
required int chatId,
int chatType = 1,
}) async {
// 1. 文件大小
final file = File(filePath);
int size = 0;
try {
size = await file.length();
} catch (e) {
debugPrint('[SendVideoUseCase] 获取文件大小失败: $e');
}
// 2. 上传
String uploadedUrl = '';
try {
final result = await _apiClient.executeRequest(
UploadFileRequest(filePath: filePath),
);
uploadedUrl = result?.url ?? '';
} catch (e) {
debugPrint('[SendVideoUseCase] upload error: $e');
rethrow;
}
if (uploadedUrl.isEmpty) {
throw Exception('[SendVideoUseCase] upload returned empty url');
}
// 3. 发送 typ=4 消息
final content = jsonEncode({
'url': uploadedUrl,
'thumb': '', // 视频缩略图(待后续接入 video_thumbnail
'size': size,
});
await _sendMessage.execute(
chatId: chatId,
content: content,
typ: 4,
);
}
}