Files
customer-im-client-dev/Doc/image_grid_architecture.md
pp-bot 2354e92c64 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>
2026-03-24 14:09:27 +09:00

145 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多图宫格气泡架构文档
对应 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