- #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>
145 lines
4.4 KiB
Markdown
145 lines
4.4 KiB
Markdown
# 多图宫格气泡架构文档
|
||
|
||
对应 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 |
|
||
| 3–9 | 3 列 | 78 × 78 pt | 3 pt |
|
||
|
||
- **外层圆角**:8 pt(ClipRRect)
|
||
- **单格圆角**:4 pt
|
||
- **最后一行靠左**:不足 3 格时用透明占位补齐(仅 3 列布局)
|
||
- **总宽度**:2 列 = 235 pt;3 列 = 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)
|