Files
customer-im-client-dev/Doc/emoji_sticker_attachment_architecture.md
pp-bot 21b7201590 docs(chat): 表情/贴纸/附件面板架构文档(#51~#56)
StickerMessageBubble/AttachmentPanelSheet/EmojiPanel/SendVideoUseCase
完整架构说明:组件设计、CDN路径规则、DI装配、待完成事项。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:18:36 +09:00

5.9 KiB
Raw Permalink Blame History

表情 / 贴纸 / 附件面板架构设计文档

Gitea Issues: #51#56 Commit: bb9f1aa 参考iOS StickerMessageBubble, AttachmentPanelView, EmojiKeyboardView


一、功能范围

Issue 功能 状态
#51 StickerMessageBubbletyp=5无边框 120pt CDN 图)
#52 AttachmentPanelSheet6 格面板)
#53 拍照发送image_picker.camera → SendImageUseCase
#54 SendVideoUseCasevideo picker → CDN 上传 → typ=4 发送)
#55 文件发送file_picker 占位SnackBar 提示) 待接入
#56 EmojiPanel4 分类 ~256 emoji光标插入⌫ 退格)

二、架构概览

ChatDetailPage
├── _InputBar
│   ├── [😊] EmojiPanel.show()      ← 全屏 DraggableScrollableSheet
│   └── [📎] AttachmentPanelSheet.show()  ← BottomSheet Future<AttachmentOption?>
│
├── _buildBubble(typ)
│   └── case 5 → StickerMessageBubble
│
└── State handlers
    ├── _showEmojiPanel()    → EmojiPanel (插入 / 退格)
    ├── _showAttachmentPanel() → dispatch AttachmentOption
    ├── _pickFromCamera()    → image_picker → SendImageUseCase
    └── _pickVideo()         → image_picker → SendVideoUseCase

三、组件详解

3.1 StickerMessageBubbletyp=5

文件features/chat/view/widgets/sticker_message_bubble.dart

class StickerMessageBubble extends StatelessWidget {
  final String rawContent; // {"url":"sticker/xxx.png","width":120,"height":120}
  static const double _maxSize = 120.0;
}
  • rawContent JSON 解析:urlCdnUrlResolver.resolve(url) → CDN 完整 URL
  • 无气泡背景(BoxDecoration 不设置颜色)
  • 固定 120×120pt SizedBoxImage.network + BoxFit.contain
  • ClipRRect(borderRadius: 8) 防止圆角溢出

CDN 路径规则CdnUrlResolver

前缀 解析结果
sticker/ AppConfig.apiBaseUrl/sticker/...
Image/ AppConfig.apiBaseUrl/Image/...
http(s):// 原样透传

3.2 AttachmentPanelSheet#52

文件features/chat/view/widgets/attachment_panel_sheet.dart

enum AttachmentOption { camera, gallery, video, file, voice, redEnvelope }

class AttachmentPanelSheet extends StatelessWidget {
  static Future<AttachmentOption?> show(BuildContext context) {
    return showModalBottomSheet<AttachmentOption>(...);
  }
}
  • GridView.count(crossAxisCount: 3)2 行 6 格
  • 每格56pt 着色圆形图标 + 标签文字
  • 颜色方案:
选项 色号
拍照 #5667FF
相册 #0BB8A9
视频 #FF5FA2
文件 #FF8B5E
录音 #8A5CF6
红包 #E8600A
  • 返回 Future<AttachmentOption?> — 父页面负责所有业务逻辑,面板本身无状态

3.3 EmojiPanel#56

文件features/chat/view/widgets/emoji_panel.dart

class EmojiPanel extends StatefulWidget {
  static Future<void> show(BuildContext context, {
    required void Function(String) onEmojiSelected,
    required VoidCallback onBackspace,
  }) { ... }
}
  • DraggableScrollableSheet 从底部展开,高度 40% 屏高
  • 4 个分类 Tab😊 常用80/ 👤 人物54/ 🌿 自然72/ 🎯 物品77
  • GridView.count(crossAxisCount: 8) 展示 emoji
  • 退格键(⌫):text.runes.toList() 正确处理多码点 emoji👨‍👩‍👧
  • 光标插入:TextEditingController.selection.baseOffset.clamp(0, text.length)

3.4 SendVideoUseCase#54

文件features/chat/usecases/send_video_usecase.dart

video picker (image_picker.pickVideo)
    ↓
File.length() → size (bytes)
    ↓
UploadFileRequest(filePath) → CDN URL
    ↓
SendMessageUseCase(typ: 4, content: {"url":"...","thumb":"","size":N})
  • thumb 当前为空字符串(video_thumbnail 包未接入)
  • VideoMessageBubble 已处理 thumb 为空的情况(显示播放图标占位)
  • chatType 默认 1单聊

四、ChatDetailPage 集成

4.1 _InputBar 改动

// 新增两个按钮emoji + attach
Row(children: [
  IconButton(icon: Icon(Icons.emoji_emotions_outlined), onPressed: onEmoji),
  IconButton(icon: Icon(Icons.attach_file), onPressed: onAttachPanel),
  Expanded(child: TextField(...)),
  IconButton(icon: Icon(Icons.send), onPressed: onSend),
])

4.2 附件派发流程

Future<void> _showAttachmentPanel() async {
  final opt = await AttachmentPanelSheet.show(context);
  switch (opt) {
    case AttachmentOption.camera:   _pickFromCamera(); break;
    case AttachmentOption.gallery:  _showImagePicker(); break;
    case AttachmentOption.video:    _pickVideo(); break;
    case AttachmentOption.file:     // SnackBar: 即将上线
    case AttachmentOption.voice:    // SnackBar: 即将上线
    case AttachmentOption.redEnvelope: // SnackBar: 即将上线
  }
}

4.3 typ=5 气泡路由

Widget _buildBubble(int typ, String content, bool isMine) {
  switch (typ) {
    case 1: return _TextBubble(...)
    case 2: return ImageMessageBubble(...)
    case 4: return VideoMessageBubble(...)
    case 5: return StickerMessageBubble(rawContent: content)  // ← #51
    case 6: return FileMessageBubble(...)
    ...
  }
}

五、DI 装配

文件features/chat/di/chat_service_providers.dart

final sendVideoUseCaseProvider = Provider<SendVideoUseCase>((ref) {
  return SendVideoUseCase(
    apiClient: ref.read(networkSdkApiProvider),
    sendMessage: ref.read(sendMessageUseCaseProvider),
  );
});

六、待完成事项

Issue 内容 前提条件
#55 文件发送完整实现 flutter pub add file_picker
录音发送VoiceRecordSheet 录音 API 对接
附件面板红包入口接入 RedEnvelope SendView 接入
视频缩略图thumb flutter pub add video_thumbnail