docs: add native ui phase 2 spec and plan
This commit is contained in:
364
docs/superpowers/plans/2026-03-27-wechat-native-ui-phase2.md
Normal file
364
docs/superpowers/plans/2026-03-27-wechat-native-ui-phase2.md
Normal file
@@ -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"
|
||||
```
|
||||
Reference in New Issue
Block a user