- #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>
4.4 KiB
4.4 KiB
多图宫格气泡架构文档
对应 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 消息:
{"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)