# 多图宫格气泡架构文档 对应 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)