# 多图宫格气泡架构文档

对应 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）
