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

4.4 KiB
Raw Blame History

多图宫格气泡架构文档

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