Files
Arkie-Library-Frontend/.unipi/docs/plans/2026-05-25-telegram-style-resource-stream-plan.md

211 lines
12 KiB
Markdown
Raw Normal View History

---
title: "Telegram-style Resource Stream — Implementation Plan"
type: plan
date: 2026-05-25
workbranch: feat/telegram-stream
specs:
- .unipi/docs/specs/2026-05-25-telegram-style-resource-stream-design.md
---
# Telegram-style Resource Stream — Implementation Plan
## Overview
实现 `/browse``/category/:slug` 的 Telegram-style 消息流重构。前端先用 mock data 完成全部视觉与交互,等后端 `/api/posts` 系列接口 ready 后切换。同时收尾删除收藏功能与 `ResourceDetail` 详情页。
分支:`feat/telegram-stream`(在当前目录新建,不走 worktree。完成后由 Terry 显式确认才 merge。
## Sequencing
```
Task 1 (基础类型 + mock + utils)
Task 2 (hooks) ─────────────────┐
Task 3 (overlays) ─────┐ │
↓ │
Task 4 (bubbles)
Task 5 (stream 容器)
Task 6 (页面改写)
Task 7 (清理收藏 / 详情页) ── 可与 1-5 并行,但合并到 Task 6 之前完成
Task 8 (验证 + 文档 + API 契约) ── 最后
```
依赖关键路径1 → 2/3 → 4 → 5 → 6 → 8。Task 7 独立,建议早做以减少 imports 残留。
## Tasks
- unstarted: Task 0 — 创建分支
- Description: 在当前目录创建并切到 `feat/telegram-stream` 分支
- Dependencies: 无
- Acceptance Criteria: `git branch --show-current` 输出 `feat/telegram-stream``git status` 干净
- Steps:
1. `git status --short --branch` 确认无未提交改动
2. `git checkout -b feat/telegram-stream`
3. 确认 `.unipi/docs/specs/2026-05-25-telegram-style-resource-stream-design.md``.unipi/docs/plans/2026-05-25-telegram-style-resource-stream-plan.md` 已在该分支
- unstarted: Task 1 — 类型定义 + Mock data + 纯函数 utils
- Description: 建立 Post / Attachment 类型,写覆盖 7 种 bubble 的 mock 样本,写 3 个无依赖纯函数 util
- Dependencies: Task 0
- Acceptance Criteria:
- `src/types/post.ts` 导出 `Post``Attachment``PostListResponse``PostScope`
- `src/mocks/mockPosts.ts` 至少 12 条 Post 覆盖2 图片当文档 + 2 PDF/AI + 2 纯文本+链接 + 1 视频 + 2 单图 + 1 图+文 + 1 三图相册 + 1 七图相册publishedAt 跨 ≥3 个不同日期
- `formatBytes(3549239)``"3.4 MB"`unit tests pass
- `autolink("点 https://x.com 看")` 返回 React 节点数组,链接处为 `<a target="_blank" rel="noopener noreferrer">`unit tests pass
- `fileIcon({ mime, filename })` 返回 `{ Icon, color }`PDF 红、AI 橙、PPT 红、图片走 thumbnail 不返回 Icon
- `npx tsc --noEmit` 通过
- Steps:
1.`src/types/post.ts`
2.`src/components/messageStream/utils/formatBytes.ts` + `.test.ts`
3.`src/components/messageStream/utils/autolink.tsx` + `.test.tsx`
4.`src/components/messageStream/utils/fileIcon.ts`
5.`src/mocks/mockPosts.ts`(图片用 picsum.photos 占位,视频用公开 sample mp4 + 一张占位 poster
6.`npm test``npx tsc --noEmit`
- unstarted: Task 2 — Stream hooksusePostStream + useGroupedByDay
- Description: 数据层抽象mock/real 双模式 + 按日期分组
- Dependencies: Task 1
- Acceptance Criteria:
- `usePostStream({ scope, type, language })``VITE_USE_MOCK_POSTS !== "false"` 时从 `MOCK_POSTS` 过滤 + 倒序 + cursor 切片(每页 20+ 200ms 假延迟
- 真接口分支调用 `getJSON</api/posts?...>`(占位即可,后端未 ready 时不会走到)
- 返回 `{ items, isLoading, error, hasMore, loadMore, reset }`
- `useGroupedByDay(posts, lang)` 返回 `Array<{ dayLabel: string; items: Post[] }>`按本地时区日期分组dayLabel 通过 `Intl.DateTimeFormat` 按 lang 切换zh-TW/zh-CN/en
- 单元测试useGroupedByDay 在跨天数据上分出正确组数
- Steps:
1.`src/components/messageStream/hooks/usePostStream.ts`
2.`src/components/messageStream/hooks/useGroupedByDay.ts` + `.test.ts`
3.`.env.example``VITE_USE_MOCK_POSTS=true` 注释
4.`npm test``npx tsc --noEmit`
- unstarted: Task 3 — Overlay 基础设施ImageLightbox + VideoPlayer
- Description: 全屏画廊与视频播放器portal 渲染,单一入口 context
- Dependencies: Task 1
- Acceptance Criteria:
- `<ImageLightboxProvider>` 包在 App 根,暴露 `useLightbox()``openLightbox(images, startIndex)`
- `ImageLightbox` 支持:左右切换(按钮 + 键盘 ← → + 触屏左右滑、ESC/点遮罩关闭、右上角下载按钮
- `VideoPlayer` 支持:全屏遮罩 + `<video controls autoPlay>`、接 `currentTime` 参数避免重新加载、ESC/点遮罩关闭
- 两个 overlay 在手机端不溢出、不被底部 nav 遮挡
- `npx tsc --noEmit` 通过
- Steps:
1.`src/components/messageStream/overlays/ImageLightbox.tsx` + Provider/Context
2.`src/components/messageStream/overlays/VideoPlayer.tsx`
3.`App.tsx` / 根布局挂 Provider
4. 手动验证:在 dev console 临时调用 `openLightbox` 看是否正确呈现
- unstarted: Task 4 — Bubble 子组件6 个 + 分发器)
- Description: 按截图实现 6 种气泡 + `MessageBubble` 分发
- Dependencies: Task 1, Task 3
- Acceptance Criteria:
- `FileDocBubble` 处理 `kind: "document"`
-`mime.startsWith("image/")` → 左侧用 `thumbnailUrl` 缩略 + ↓ 覆盖;右侧 filename.ext + size截图 1
- 否则 → 左侧蓝圆 ↓ 图标(按 mime 取色)+ 右侧 filename + size截图 2
- `TextBubble` 渲染 `text`,调 `autolink`(截图 3
- `VideoBubble` 初始显示 posterUrl + ▶️ + 时长,第一次点 inline `<video controls autoPlay>`,第二次点(已播放)调 `openVideoPlayer`(截图 4
- `ImageBubble` 单张图,点击调 `openLightbox([att], 0)`(截图 5
- `ImageWithTextBubble` 单图 + 下方文本autolink截图 6
- `AlbumBubble` 2-4 格 grid间距 2pxattachments.length > 4 时第 4 格 `bg-black/45 backdrop-blur-sm` 覆盖 `+N`;点任一格调 `openLightbox(images, index)`(截图 7
- `MessageBubble` 实现 spec §4 的 `pickBubble` 分发,右下角绝对定位时间戳 `text-[11px] text-neutral-500`
- 所有 bubble 容器:`rounded-2xl bg-ark-panel p-3`(文本气泡 `px-4 py-2.5`),左对齐,无头像
- `npx tsc --noEmit` 通过
- Steps:
1.`MessageBubble.tsx`(含 pickBubble
2.`bubbles/TextBubble.tsx`
3.`bubbles/FileDocBubble.tsx`
4.`bubbles/ImageBubble.tsx`
5.`bubbles/ImageWithTextBubble.tsx`
6.`bubbles/AlbumBubble.tsx`
7.`bubbles/VideoBubble.tsx`
8. 在 dev 中临时挂一个 demo route 跑 MOCK_POSTS 全量渲染,目视检查 7 张截图对照
- unstarted: Task 5 — Stream 容器 + FilterChips + DaySeparator
- Description: 顶层组件接管 fetch、分组、无限滚动、sticky 筛选
- Dependencies: Task 2, Task 4
- Acceptance Criteria:
- `FilterChips`sticky top横向滚动 `overflow-x-auto whitespace-nowrap`,类型 chipsall/image/video/ppt/pdf/text/link/archive沿用 `typeFilterLabel`+ 语言 chipsall/zh-TW/zh-CN/en改变时 reset cursor 并同步 URL `?type=&language=`
- `DaySeparator`:胶囊样式,居中
- `MessageStream`
-`scope: { kind: "all" } | { kind: "category", slug: string }`
- 调用 `usePostStream` + `useGroupedByDay`
-`IntersectionObserver` 监听底部 sentinel触发 `loadMore`loadingRef 守护避免重复)
- 容器:手机 `max-w-full px-3 mx-auto`md+ `max-w-[640px] mx-auto`
- 空状态用 `t("noResults")`,错误用红色 inline 横幅 + 重试按钮
- `npx tsc --noEmit` 通过
- Steps:
1.`FilterChips.tsx`
2.`DaySeparator.tsx`
3.`MessageStream.tsx`
4. 在 dev demo route 挂 `<MessageStream scope={{ kind: "all" }} />` 验证滚动 + 筛选 + 分组
- unstarted: Task 6 — 重写 CategoryPage 与 Browse
- Description: 两个页面瘦身为单一组件调用
- Dependencies: Task 5, Task 7
- Acceptance Criteria:
- `src/pages/CategoryPage.tsx`:仅渲染分类标题 + `<MessageStream scope={{ kind: "category", slug }} />`,行数 < 30
- `src/pages/Browse.tsx`:仅渲染页面标题 + `<MessageStream scope={{ kind: "all" }} />`,行数 < 30不再读 `sort` / `tag` / `page` 参数
- 排序 tabs 全部去掉
- 移除对 `ResourceCard` / `ResourceListFooter` 的 import
- `App.tsx` 路由保持 `/browse``/category/:slug` 不变
- `npx tsc --noEmit` 通过
- Steps:
1. 改写 `CategoryPage.tsx`
2. 改写 `Browse.tsx`
3. 跑 dev server 在 `/browse``/category/<slug>` 验证
- unstarted: Task 7 — 移除收藏功能 + ResourceDetail
- Description: 整套清理
- Dependencies: Task 0可与 Task 1-5 并行)
- Acceptance Criteria:
- 删除文件:`src/pages/FavoritesPage.tsx``src/pages/ResourceDetail.tsx``src/components/ResourceCard.tsx``src/components/ResourceListFooter.tsx`
- `App.tsx` 移除 `/favorites` 路由;`/r/:id` 改为新组件 `PostRedirect`mock 模式下从 `MOCK_POSTS` 找 slug找不到 → navigate `/browse`
- `src/api.ts` 移除 `postFavoriteDelta`
- 全代码无 `postFavoriteDelta` / `FavoritesPage` / `ResourceDetail` / `ResourceCard` / `ResourceListFooter` 引用
- Home 中的 `/favorites` 入口(如有)移除
- `src/i18n.tsx` 移除 `favorites` / `addFavorite` / `removeFavorite` 等收藏 key三语言同步
- `npx tsc --noEmit` 通过(无 unused / 未引用错误)
- Steps:
1. `grep -rn "postFavoriteDelta\|FavoritesPage\|ResourceDetail\|ResourceCard\|ResourceListFooter\|/favorites\|/r/:id" src/` 列出所有引用点
2.`src/pages/PostRedirect.tsx`,挂到 `/r/:id` 路由
3. 删除 4 个文件 + 修改 `App.tsx` + `api.ts` + `i18n.tsx`
4. 再 grep 一次确认清零
5. `npx tsc --noEmit`
- unstarted: Task 8 — 验证 + 文档 + API 契约抽出
- Description: 全量验证 + 给后端的接口文档
- Dependencies: Task 6, Task 7
- Acceptance Criteria:
- `npx tsc --noEmit` 通过
- `npm run format:check` 通过(若不通过先 `npm run format`
- `npm test` 全绿
- `npm run build` 成功
- 手动视觉验证Chrome DevTools iPhone 14 Pro 视口逐一对照 7 张参考截图
- 新增 `.unipi/docs/specs/2026-05-25-posts-api-contract.md`:从主 spec §1-§2 抽出后端需要的所有内容Post/Attachment schema、6 个 endpoint、删除清单、迁移要求
- 更新 `README.md` 增加 `VITE_USE_MOCK_POSTS` 段落
- 不 commit、不 push等 Terry 显式确认)
- Steps:
1. 跑全套检查命令
2. dev server 手机视口检查
3. 写 API 契约文档
4. 改 README
5. 报告完成,等 Terry 审阅与 commit 指令
## Risks
- **mock 数据视觉与真实数据偏差**mock 用 picsum 占位图可能掩盖真实图片不同宽高比的边界情况。缓解mockPosts 中包含横图 / 竖图 / 接近正方形三种比例样本。
- **video poster 在 mock 模式不易获取**:用一张本地 SVG 占位即可,避免依赖外部链接。
- **i18n 删除收藏 key 后未使用的引用**tsconfig 的 `noUnusedLocals` 不覆盖 i18n key 的字符串引用,需手动 grep。
- **`PostRedirect` 在真接口模式下的实现**:当前先写 mock 分支,真接口分支 TODO 注释标明等 `/api/posts/:id` ready 后补。
- **infinite scroll + URL 同步**:用户改 filter chip 时既要 reset cursor 又要更新 URL注意避免 `setSearchParams` 触发额外 effect 循环。
- **后端最终 schema 与本 spec 偏差**:如有偏差,必须先回 spec 改契约,再调前端类型,避免散点修改。
## Out of Scope本 plan 不涵盖,遵循 spec
- Home 页布局调整
- Admin 后台 Post 编辑器
- 真实后端 API 实现 + 数据迁移
- 长按菜单 / 评论 / Reaction
- 桌面端多列布局
- SEO 优化