feat: 多图宫格气泡全量实现 (#36~#38)

- #36 ImageGridBubble: 2 列×116pt / 3 列×78pt, 3pt 间距, 8pt 外层/4pt 单格圆角, tap→ImageViewerPage
- #37 ChatDisplayItem + _buildDisplayItems: 连续 typ=2 同 sendId Δt<5s 分组为宫格,iOS ChatView.buildDisplayItems parity
- #38 SendImageUseCase.sendBatch: Future.wait 并行上传 → 顺序快速发送,ImagePickerSheet 改用 sendBatch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
pp-bot
2026-03-24 14:09:27 +09:00
parent b971900263
commit 2354e92c64
5 changed files with 551 additions and 40 deletions

View File

@@ -0,0 +1,144 @@
# 多图宫格气泡架构文档
对应 Gitea issues #36#38
---
## 1. 背景与目标
iOS 老项目 (`ImageGridBubble.swift`, `ChatView.buildDisplayItems()`) 已实现:
- 接收端:连续同发送者图片消息自动合并为宫格气泡
- 发送端:多图并行上传 → 快速连续发送 → 满足宫格分组条件
Flutter 新项目补齐以下能力:
| 功能 | Issue | iOS 对应 |
|------|-------|---------|
| 宫格气泡展示 | #36 | `ImageGridBubble.swift` |
| 消息分组逻辑 | #37 | `ChatView.buildDisplayItems()` |
| 多图并行发送 | #38 | `PhotosPicker + parallel upload` |
---
## 2. 消息格式
多图不引入新的 typ。每张图片仍为独立的 **typ=2** 消息:
```json
{"url":"Image/xxx.jpg","width":1024,"height":768}
```
分组完全在 **客户端渲染层** 完成,服务端/DB 无感知。
---
## 3. 宫格布局规则iOS 对齐)
| 图片数 | 列数 | 单格尺寸 | 间距 |
|--------|------|----------|------|
| 2 | 2 列 | 116 × 116 pt | 3 pt |
| 39 | 3 列 | 78 × 78 pt | 3 pt |
- **外层圆角**8 ptClipRRect
- **单格圆角**4 pt
- **最后一行靠左**:不足 3 格时用透明占位补齐(仅 3 列布局)
- **总宽度**2 列 = 235 pt3 列 = 240 pt
---
## 4. 分组算法iOS ChatView.buildDisplayItems 对齐)
```
for i in msgs:
if msgs[i].typ == 2:
batch = [msgs[i]]
j = i + 1
while j < len(msgs) and len(batch) < 9:
next = msgs[j]
if next.typ == 2
and next.sendId == batch[0].sendId
and (next.sendTime - msgs[j-1].sendTime) < 5:
batch.append(next)
j++
else:
break
if len(batch) >= 2:
emit ChatDisplayItem(grid=batch)
i = j
continue
emit ChatDisplayItem(single=msgs[i])
i++
```
关键规则:
- 同一 `sendId`(不区分我发/他发)
- 连续相邻消息时间差 < **5 秒**(不是与第一条比,是与前一条比)
- 最多 **9 条** 一组,第 10 条另起新组
- typ 不为 2 立即截断分组
---
## 5. 并行上传设计(#38
```
用户点击「发送」
└─ SendImageUseCase.sendBatch(filePaths, chatId)
├─ Future.wait([upload(img1), upload(img2), ..., upload(imgN)])
│ └─ 每个 upload: readBytes → dart:ui.ImageDescriptor → UploadFileRequest → url
└─ for url in results (顺序):
└─ SendMessageUseCase.execute(chatId, jsonEncode({url,w,h}), typ=2)
└─ 乐观写入 sendTime = now()(各条仅相差毫秒)
```
**关键**:并行上传确保所有 `sendTime` 在同一时刻附近(< 1 秒差),
满足分组条件(< 5 秒)。
---
## 6. 数据流:发送多图
```
ImagePickerSheet._sendAll()
└─ sendImageUseCaseProvider.sendBatch(filePaths, chatId)
├─ 并行上传 → [(url1,w1,h1), (url2,w2,h2), ...]
└─ 顺序发送 → [typ=2 msg1, typ=2 msg2, ...]
└─ DB Stream → messagesByChatIdProvider
└─ _buildDisplayItems()
└─ [msg1, msg2] 同 sendId + typ=2 + Δt<5s
└─ ChatDisplayItem(grid=[msg1, msg2])
└─ ImageGridBubble(messages: [msg1, msg2])
```
---
## 7. 数据流:接收多图
```
WS/HTTP → DB → messagesByChatIdProvider Stream
└─ _buildDisplayItems(msgs)
└─ 连续 typ=2 同 sendId Δt<5s → ChatDisplayItem(grid)
└─ ImageGridBubble
├─ GridCell(Image.network + ClipRRect 4pt)
└─ onTap → ImageViewerPage.open(urls: allUrls, initialIndex: i)
```
---
## 8. 新增/修改文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `lib/features/chat/view/widgets/image_grid_bubble.dart` | 新增 | #36 宫格气泡组件 |
| `lib/features/chat/view/chat_detail_page.dart` | 修改 | #37 ChatDisplayItem + buildDisplayItems |
| `lib/features/chat/usecases/send_image_usecase.dart` | 修改 | #38 sendBatch() 并行上传 |
| `lib/features/chat/view/widgets/image_picker_sheet.dart` | 修改 | #38 改用 sendBatch() |
| `Doc/image_grid_architecture.md` | 新增 | 本文档 |
---
## 9. 待完成
- 图片 sticker (typ=5) 展示
- GIF 消息 (typ=25) 支持(目前 typ=25 走 ImageMessageBubble后续可合并入宫格分组
- 端到端加密图片cipher_guard_sdk 接入后)
- 宫格内视频支持(若后续 album 含 video