diff --git a/docs/superpowers/plans/2026-03-27-wechat-native-ui-phase2.md b/docs/superpowers/plans/2026-03-27-wechat-native-ui-phase2.md new file mode 100644 index 0000000..0e49212 --- /dev/null +++ b/docs/superpowers/plans/2026-03-27-wechat-native-ui-phase2.md @@ -0,0 +1,364 @@ +# WeChat Native UI Phase 2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Polish the native Android WeChat-style rollback with better chat feel, clearer OTA download/install feedback, and smoother root navigation memory. + +**Architecture:** Keep the approved WeChat-style root surfaces intact and limit this batch to interaction polish. Add small pure-Java helpers for chat UI state and OTA download state so the tricky behavior is unit-tested, then wire those helpers into `ProjectDetailActivity`, `AboutActivity`, and `MainActivity`. + +**Tech Stack:** Android AppCompat, XML layouts, Java 21, Gradle 8, JUnit4, DownloadManager, existing Boss APIs, existing release packaging and deploy scripts. + +--- + +## File Structure + +### New files + +- `android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java` + - Pure-Java helper for send-button enabled state, optimistic pending bubble state, and auto-scroll policy. +- `android/app/src/main/java/com/hyzq/boss/OtaDownloadStateMapper.java` + - Pure-Java helper for mapping `DownloadManager` progress/status into UI strings and retry/install states. +- `android/app/src/main/java/com/hyzq/boss/RootTabMemory.java` + - Pure-Java helper for resolving explicit root tabs versus stored root tabs. +- `android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java` + - Unit tests for chat send/pending/scroll rules. +- `android/app/src/test/java/com/hyzq/boss/OtaDownloadStateMapperTest.java` + - Unit tests for OTA download state mapping. +- `android/app/src/test/java/com/hyzq/boss/RootTabMemoryTest.java` + - Unit tests for remembered root tab selection. + +### Modified files + +- `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java` +- `android/app/src/main/java/com/hyzq/boss/AboutActivity.java` +- `android/app/src/main/java/com/hyzq/boss/MainActivity.java` +- `android/app/src/main/java/com/hyzq/boss/BossUi.java` +- `README.md` +- `docs/architecture/current_runtime_and_deploy_status_cn.md` +- `docs/architecture/api_and_service_inventory_cn.md` + +## Task 1: Polish Chat Composer Feedback and Scroll Behavior + +**Files:** +- Create: `android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java` +- Create: `android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java` +- Modify: `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java` +- Modify: `android/app/src/main/java/com/hyzq/boss/BossUi.java` + +- [ ] **Step 1: Write the failing test** + +```java +package com.hyzq.boss; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ProjectChatUiStateTest { + @Test + public void sendEnabled_requiresTextAndNotBusy() { + assertFalse(ProjectChatUiState.canSend("", false)); + assertFalse(ProjectChatUiState.canSend(" ", false)); + assertFalse(ProjectChatUiState.canSend("你好", true)); + assertTrue(ProjectChatUiState.canSend("你好", false)); + } + + @Test + public void shouldAutoScroll_onlyWhenNearBottomOrForced() { + assertTrue(ProjectChatUiState.shouldAutoScroll(true, false)); + assertTrue(ProjectChatUiState.shouldAutoScroll(false, true)); + assertFalse(ProjectChatUiState.shouldAutoScroll(false, false)); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd /Users/kris/code/boss +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon +``` + +Expected: FAIL with missing `ProjectChatUiState`. + +- [ ] **Step 3: Write minimal implementation** + +```java +package com.hyzq.boss; + +public final class ProjectChatUiState { + private ProjectChatUiState() {} + + public static boolean canSend(String text, boolean sending) { + return !sending && text != null && !text.trim().isEmpty(); + } + + public static boolean shouldAutoScroll(boolean nearBottom, boolean forced) { + return nearBottom || forced; + } +} +``` + +Then integrate it into `ProjectDetailActivity`: + +- disable send button when text empty or request busy +- show a local pending outgoing bubble immediately after tapping send +- only auto-scroll on refresh when user was already near bottom or after local send + +- [ ] **Step 4: Run tests and compile verification** + +Run: + +```bash +cd /Users/kris/code/boss +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android :app:compileDebugJavaWithJavac --no-daemon +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/kris/code/boss +git add android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java \ + android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java \ + android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java \ + android/app/src/main/java/com/hyzq/boss/BossUi.java +git commit -m "feat: polish native chat composer feedback" +``` + +## Task 2: Add OTA Download Progress and Retry Guidance + +**Files:** +- Create: `android/app/src/main/java/com/hyzq/boss/OtaDownloadStateMapper.java` +- Create: `android/app/src/test/java/com/hyzq/boss/OtaDownloadStateMapperTest.java` +- Modify: `android/app/src/main/java/com/hyzq/boss/AboutActivity.java` +- Modify: `android/app/src/main/java/com/hyzq/boss/BossUi.java` + +- [ ] **Step 1: Write the failing test** + +```java +package com.hyzq.boss; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class OtaDownloadStateMapperTest { + @Test + public void toProgressLabel_formatsKnownProgress() { + assertEquals("已下载 50%", OtaDownloadStateMapper.toProgressLabel(50, true)); + } + + @Test + public void toProgressLabel_handlesUnknownProgress() { + assertEquals("正在准备下载", OtaDownloadStateMapper.toProgressLabel(0, false)); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd /Users/kris/code/boss +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.OtaDownloadStateMapperTest --no-daemon +``` + +Expected: FAIL with missing `OtaDownloadStateMapper`. + +- [ ] **Step 3: Write minimal implementation** + +```java +package com.hyzq.boss; + +public final class OtaDownloadStateMapper { + private OtaDownloadStateMapper() {} + + public static String toProgressLabel(int percent, boolean hasKnownTotal) { + if (!hasKnownTotal) { + return "正在准备下载"; + } + return "已下载 " + Math.max(0, Math.min(100, percent)) + "%"; + } +} +``` + +Then integrate it into `AboutActivity`: + +- query `DownloadManager` while download is active +- render a visible progress row in page content +- keep a retry action after failed download +- show install permission guidance row when unknown-source install permission is missing + +- [ ] **Step 4: Run tests and compile verification** + +Run: + +```bash +cd /Users/kris/code/boss +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.OtaDownloadStateMapperTest --no-daemon +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android :app:compileDebugJavaWithJavac --no-daemon +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/kris/code/boss +git add android/app/src/main/java/com/hyzq/boss/OtaDownloadStateMapper.java \ + android/app/src/test/java/com/hyzq/boss/OtaDownloadStateMapperTest.java \ + android/app/src/main/java/com/hyzq/boss/AboutActivity.java \ + android/app/src/main/java/com/hyzq/boss/BossUi.java +git commit -m "feat: add native ota progress feedback" +``` + +## Task 3: Remember Root Tab and Smooth Root Back Behavior + +**Files:** +- Create: `android/app/src/main/java/com/hyzq/boss/RootTabMemory.java` +- Create: `android/app/src/test/java/com/hyzq/boss/RootTabMemoryTest.java` +- Modify: `android/app/src/main/java/com/hyzq/boss/MainActivity.java` + +- [ ] **Step 1: Write the failing test** + +```java +package com.hyzq.boss; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class RootTabMemoryTest { + @Test + public void resolveInitialTab_prefersExplicitTab() { + assertEquals("devices", RootTabMemory.resolveInitialTab("devices", "me")); + } + + @Test + public void resolveInitialTab_fallsBackToStoredTab() { + assertEquals("me", RootTabMemory.resolveInitialTab(null, "me")); + } + + @Test + public void resolveInitialTab_defaultsToConversations() { + assertEquals("conversations", RootTabMemory.resolveInitialTab(null, null)); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run the new unit test target after creating it in the same package: + +```bash +cd /Users/kris/code/boss +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.RootTabMemoryTest --no-daemon +``` + +Expected: FAIL with missing helper. + +- [ ] **Step 3: Write minimal implementation** + +Implement the helper: + +```java +package com.hyzq.boss; + +public final class RootTabMemory { + private RootTabMemory() {} + + public static String resolveInitialTab(String explicitTab, String storedTab) { + if ("conversations".equals(explicitTab) || "devices".equals(explicitTab) || "me".equals(explicitTab)) { + return explicitTab; + } + if ("conversations".equals(storedTab) || "devices".equals(storedTab) || "me".equals(storedTab)) { + return storedTab; + } + return "conversations"; + } +} +``` + +Then wire it into `MainActivity` so that: + +- last selected root tab is persisted in `SharedPreferences` +- explicit deep-link tab still wins over stored tab +- when already at root conversations tab, back key shows a soft toast then moves task to back + +- [ ] **Step 4: Run tests and full debug verification** + +Run: + +```bash +cd /Users/kris/code/boss +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --no-daemon +JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android assembleDebug --no-daemon +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/kris/code/boss +git add android/app/src/main/java/com/hyzq/boss/MainActivity.java \ + android/app/src/main/java/com/hyzq/boss/RootTabMemory.java \ + android/app/src/test/java/com/hyzq/boss/RootTabMemoryTest.java +git commit -m "feat: polish native root tab memory" +``` + +## Task 4: Verification, Packaging, Deploy, and Docs + +**Files:** +- Modify: `README.md` +- Modify: `docs/architecture/current_runtime_and_deploy_status_cn.md` +- Modify: `docs/architecture/api_and_service_inventory_cn.md` +- Modify: `android/app/build.gradle` + +- [ ] **Step 1: Update docs and version** + +- bump Android version for the next polish release +- document chat send feedback, OTA progress, and root tab memory behavior + +- [ ] **Step 2: Run local verification** + +Run: + +```bash +cd /Users/kris/code/boss +npm run lint +npm run build +curl -sS http://127.0.0.1:3000/api/health +curl -sS http://127.0.0.1:4317/health +JAVA_HOME=$(/usr/libexec/java_home) npm run apk:release +JAVA_HOME=$(/usr/libexec/java_home) npm run aab:release +``` + +- [ ] **Step 3: Deploy and verify** + +Run: + +```bash +cd /Users/kris/code/boss +BOSS_SERVER_PASS='your-password' ./scripts/deploy-server.sh +"$HOME/.codex/skills/boss-server-debug/scripts/server_ssh.sh" exec "curl -sS http://127.0.0.1:3000/api/health" +curl -sS https://boss.hyzq.net/api/health +curl -sS https://boss.hyzq.net/downloads/boss-android-latest.json +``` + +- [ ] **Step 4: Commit** + +```bash +cd /Users/kris/code/boss +git add README.md docs/architecture/current_runtime_and_deploy_status_cn.md \ + docs/architecture/api_and_service_inventory_cn.md android/app/build.gradle \ + public/downloads/ +git commit -m "chore: publish native ui phase 2 polish release" +``` diff --git a/docs/superpowers/specs/2026-03-27-wechat-native-ui-phase2-design.md b/docs/superpowers/specs/2026-03-27-wechat-native-ui-phase2-design.md new file mode 100644 index 0000000..f9ff742 --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-wechat-native-ui-phase2-design.md @@ -0,0 +1,111 @@ +# WeChat Native UI Phase 2 Design + +**日期:** 2026-03-27 +**范围:** 原生 Android 微信式回退后的第二批细化 +**前提:** 延续 `2026-03-27-wechat-native-ui-rollback-design.md` 已批准方向,不改一级导航、不改原生路线、不把控制台式信息重新放回主 UI + +--- + +## 1. 目标 + +这一批只做三个方向的补齐: + +1. 聊天页手感更接近常用 IM,而不是“能发消息但交互生硬” +2. OTA 下载与安装链路给出更明确的原生反馈,而不是只靠系统通知 +3. 根页导航和返回逻辑再收一轮,减少“像功能页、不像 APP”的割裂感 + +这批不新增新的一级功能,也不改 Boss 的后端技术路线。 + +## 2. 保持不变的约束 + +- 一级导航仍然固定为 `会话 / 设备 / 我的` +- 会话首页仍是微信式简单聊天列表 +- 项目聊天页仍然只保留 `项目目标 / 版本记录` 两个轻入口 +- `运维 / 审计 / 修复 / 线程详情 / 转发` 仍保留深层入口,但不回到主聊天页和一级 `我的` +- 原生 Android 仍使用 `BossApiClient + Activity + XML` +- 登录恢复、OTA API、Boss Web 部署链路都保持现有实现 + +## 3. 设计一:聊天页体验细化 + +### 3.1 现状问题 + +- 点击发送后只有整体刷新,用户会感觉“发出去了,但界面没跟手” +- 发送中没有明确状态,输入框和发送按钮的反馈太弱 +- 每次刷新后直接滚到底,用户如果在看旧消息,会被强制拉走 + +### 3.2 目标效果 + +- 发送后立即出现一条本地“发送中”气泡,先给到即时反馈 +- 服务端成功后,再用真实消息列表替换掉临时气泡 +- 如果用户本来就在底部,刷新后继续滚到底 +- 如果用户已经往上翻历史,自动刷新不能把用户强行拉回底部 +- 发送按钮在空输入、发送中这两种状态下要有明确禁用表现 + +### 3.3 设计取舍 + +- 不做真正的本地离线消息队列 +- 不做复杂的消息已读/送达双勾状态 +- 只做“发送中 -> 成功刷新”这一层最小原生体验闭环 + +## 4. 设计二:OTA 原生反馈细化 + +### 4.1 现状问题 + +- 现在已经能下载并拉起安装,但主页面上看不到清晰下载进度 +- 下载失败或权限阻塞时,提示不够聚焦 +- 用户回到关于页时,不容易知道“刚才那个下载现在走到哪一步了” + +### 4.2 目标效果 + +- 关于页能显示当前 OTA 下载状态:未开始、下载中、已完成、失败、等待安装授权 +- 下载中能看到百分比或至少看到“已下载 / 总大小”的进度文案 +- 下载失败时可直接重试,不需要用户自己猜该做什么 +- 如果安装未知来源权限没开,界面要明确告诉用户下一步去哪开 + +### 4.3 设计取舍 + +- 继续使用系统 `DownloadManager` +- 不实现后台自定义下载器 +- 不实现增量更新,只优化整包 APK 下载体验 + +## 5. 设计三:导航细节细化 + +### 5.1 现状问题 + +- 根页 tab 虽然已经变成微信式,但重新进 APP 时对用户上次停留页的记忆还不稳定 +- 根页返回虽然不会直接乱跳,但体验还可以更像成熟 APP + +### 5.2 目标效果 + +- 记住用户上次停留的根 tab,下次进 APP 优先回到该 tab +- 如果外部显式指定入口 tab,仍然以显式入口优先 +- 在根页按返回时,不再像“页面崩掉式退出”;给出更柔和的退后台反馈 + +### 5.3 设计取舍 + +- 不做复杂导航栈框架迁移 +- 继续基于现有 `MainActivity` 自己维护 root tab 状态 + +## 6. 代码边界 + +这批预计新增少量纯 Java helper,目的是让关键交互可以用单元测试覆盖,而不是把逻辑都埋进 Activity: + +- `ProjectChatUiState.java` + - 负责“是否自动滚到底”“发送按钮是否可用”“临时发送中消息”的最小映射 +- `OtaDownloadStateMapper.java` + - 负责把 `DownloadManager` 查询结果映射成页面文案和状态 + +活动页主要做接线,不堆业务判断: + +- `ProjectDetailActivity.java` +- `AboutActivity.java` +- `MainActivity.java` + +## 7. 验收标准 + +1. 聊天页发送消息后,立刻能看到发送中反馈 +2. 聊天页刷新不会在用户查看历史消息时强制跳到底 +3. 关于页能看到 OTA 下载状态和失败重试入口 +4. 未知来源安装权限未开时,页面能给出明确引导 +5. APP 再次打开时,根 tab 能恢复到上次停留位置 +6. 微信式一级 UI 不被破坏,主聊天页仍然只保留 `项目目标 / 版本记录`