--- 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 节点数组,链接处为 ``(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 hooks(usePostStream + useGroupedByDay) - Description: 数据层抽象,mock/real 双模式 + 按日期分组 - Dependencies: Task 1 - Acceptance Criteria: - `usePostStream({ scope, type, language })` 在 `VITE_USE_MOCK_POSTS !== "false"` 时从 `MOCK_POSTS` 过滤 + 倒序 + cursor 切片(每页 20)+ 200ms 假延迟 - 真接口分支调用 `getJSON`(占位即可,后端未 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: - `` 包在 App 根,暴露 `useLightbox()` → `openLightbox(images, startIndex)` - `ImageLightbox` 支持:左右切换(按钮 + 键盘 ← → + 触屏左右滑)、ESC/点遮罩关闭、右上角下载按钮 - `VideoPlayer` 支持:全屏遮罩 + `