From 0fcbf2d0a095bc47b98fc8eced588c74dab25cd3 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 5 Apr 2026 13:58:08 +0800 Subject: [PATCH] fix: support archived thread search on android homepage --- .../main/java/com/hyzq/boss/MainActivity.java | 55 ++++++++++++++----- .../MainActivityConversationSearchTest.java | 29 ++++++++-- src/lib/boss-projections.ts | 15 +++++ tests/conversation-home-items.test.ts | 20 +++++++ 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/MainActivity.java b/android/app/src/main/java/com/hyzq/boss/MainActivity.java index e52c419..8a0fdc5 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -1086,12 +1086,12 @@ public class MainActivity extends AppCompatActivity { return; } if ("folder_archive".equals(conversationType) - || (conversationSearchMode && isArchivedProjectThread(item))) { + || (conversationSearchMode && !item.optString("searchMatchLabel", "").isEmpty())) { if (folderKey.isEmpty()) { showMessage("缺少 folderKey"); return; } - openConversationFolder(folderKey, resolveConversationFolderName(item, finalDisplayRow)); + openConversationFolder(folderKey, resolveConversationFolderName(item, row)); return; } if (projectId.isEmpty()) { @@ -1105,22 +1105,14 @@ public class MainActivity extends AppCompatActivity { } } - private static boolean isArchivedProjectThread(JSONObject item) { - String folderKey = item.optString("folderKey", "").trim(); - if (folderKey.isEmpty()) { - return false; - } - return item.optInt("threadCount", 0) > 1; - } - private static String buildConversationSearchLabel(JSONObject item, WechatSurfaceMapper.ConversationRow row) { if (row == null) { return ""; } - if (isArchivedProjectThread(item)) { - String folderLabel = row.folderLabel == null ? "" : row.folderLabel.trim(); - if (!folderLabel.isEmpty()) { - return folderLabel + " / " + row.threadTitle; + if ("folder_archive".equals(item.optString("conversationType", ""))) { + String matchedThreadTitle = item.optString("searchMatchLabel", "").trim(); + if (!matchedThreadTitle.isEmpty()) { + return row.threadTitle + " / " + matchedThreadTitle; } } return row.threadTitle; @@ -1321,7 +1313,17 @@ public class MainActivity extends AppCompatActivity { continue; } if (matchesConversationQuery(item, query)) { - filtered.put(item); + JSONObject filteredItem = item; + try { + filteredItem = new JSONObject(item.toString()); + String matchLabel = resolveConversationSearchMatchLabel(item, query); + if (!matchLabel.isEmpty()) { + filteredItem.put("searchMatchLabel", matchLabel); + } + } catch (Exception ignored) { + // Keep the original item if JSON cloning fails. + } + filtered.put(filteredItem); } } return filtered; @@ -1426,6 +1428,9 @@ public class MainActivity extends AppCompatActivity { if (query.isEmpty()) { return true; } + if (!resolveConversationSearchMatchLabel(item, query).isEmpty()) { + return true; + } String[] fields = new String[] { item.optString("projectTitle", ""), item.optString("threadTitle", ""), @@ -1441,6 +1446,26 @@ public class MainActivity extends AppCompatActivity { return false; } + private static String resolveConversationSearchMatchLabel(JSONObject item, String query) { + if (item == null || query == null || query.isEmpty()) { + return ""; + } + if (!"folder_archive".equals(item.optString("conversationType", ""))) { + return ""; + } + JSONArray searchAliases = item.optJSONArray("searchAliases"); + if (searchAliases == null) { + return ""; + } + for (int i = 0; i < searchAliases.length(); i++) { + String alias = searchAliases.optString(i, "").trim(); + if (!alias.isEmpty() && alias.toLowerCase().contains(query)) { + return alias; + } + } + return ""; + } + private void renderDevicesRoot() { if (screenList == null) { return; diff --git a/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java b/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java index 966e935..73cca64 100644 --- a/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java @@ -49,6 +49,27 @@ public class MainActivityConversationSearchTest { assertEquals("p2", filteredByFolder.optJSONObject(0).optString("projectId", "")); } + @Test + public void filterConversationItemsMatchesFolderArchiveSearchAliases() throws Exception { + JSONArray source = new JSONArray() + .put(new JSONObject() + .put("projectId", "folder-boss") + .put("conversationType", "folder_archive") + .put("folderKey", "mac-studio:boss") + .put("projectTitle", "Boss") + .put("threadTitle", "Boss") + .put("folderLabel", "2 个线程 · 最近:发布回滚") + .put("searchAliases", new JSONArray().put("发布回滚").put("Android UI 收尾")) + .put("lastMessagePreview", "最近:发布回滚") + .put("latestReplyLabel", "11:00")); + + JSONArray filtered = MainActivity.filterConversationItems(source, "发布回滚"); + + assertEquals(1, filtered.length()); + assertEquals("folder-boss", filtered.optJSONObject(0).optString("projectId", "")); + assertEquals("发布回滚", filtered.optJSONObject(0).optString("searchMatchLabel", "")); + } + @Test public void conversationsHeader_usesSearchIconAndPlusButton() throws Exception { MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get(); @@ -103,15 +124,15 @@ public class MainActivityConversationSearchTest { MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get(); ReflectionHelpers.setField(activity, "conversationsData", new JSONArray() .put(new JSONObject() - .put("projectId", "boss-thread-1") - .put("conversationType", "single_device") + .put("projectId", "folder-boss") + .put("conversationType", "folder_archive") .put("folderKey", "mac-studio:boss") .put("folderLabel", "Boss") .put("projectTitle", "Boss") - .put("threadTitle", "发布回滚") + .put("threadTitle", "Boss") .put("lastMessagePreview", "最近:发布回滚") .put("latestReplyLabel", "11:00") - .put("threadCount", 2))); + .put("searchAliases", new JSONArray().put("发布回滚").put("Android UI 收尾")))); ReflectionHelpers.callInstanceMethod(activity, "showContent"); Shadows.shadowOf(activity.getMainLooper()).idle(); diff --git a/src/lib/boss-projections.ts b/src/lib/boss-projections.ts index 653cd86..e22e7f9 100644 --- a/src/lib/boss-projections.ts +++ b/src/lib/boss-projections.ts @@ -44,6 +44,7 @@ export interface ConversationItem { folderLabel: string; folderKey?: string; threadCount?: number; + searchAliases?: string[]; preview: string; lastMessagePreview: string; activityIconCount: number; @@ -467,6 +468,19 @@ function sortConversationItems(items: ConversationItem[]) { }); } +function buildFolderSearchAliases(items: ConversationItem[]) { + const aliases: string[] = []; + const seen = new Set(); + for (const item of items) { + const alias = (item.threadTitle?.trim() || item.projectTitle?.trim() || "").trim(); + if (!alias || seen.has(alias)) continue; + aliases.push(alias); + seen.add(alias); + if (aliases.length >= 4) break; + } + return aliases.length > 0 ? aliases : undefined; +} + export function getConversationItems(state: BossState): ConversationItem[] { const conversations = state.projects.map((project) => buildConversationItem(state, project)); @@ -541,6 +555,7 @@ export function getConversationHomeItems(state: BossState): ConversationItem[] { folderLabel: recentThreadLabel ? `${items.length} 个线程 · 最近:${recentThreadLabel}` : `${items.length} 个线程`, folderKey, threadCount: items.length, + searchAliases: buildFolderSearchAliases(items), preview: latestItem.preview || `包含 ${items.length} 个线程,最近活跃:《${latestItem.threadTitle}》`, lastMessagePreview: diff --git a/tests/conversation-home-items.test.ts b/tests/conversation-home-items.test.ts index 22d28d5..f484776 100644 --- a/tests/conversation-home-items.test.ts +++ b/tests/conversation-home-items.test.ts @@ -424,6 +424,7 @@ test("folder archive homepage rows keep the project title, compact subtitle, and const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive"); assert.ok(folder, "expected grouped folder archive item"); + assert.deepEqual(folder?.searchAliases, ["发布回滚", "归档确认"]); const presentation = getConversationListItemPresentation({ ...folder!, projectTitle: "项目标题", @@ -435,6 +436,25 @@ test("folder archive homepage rows keep the project title, compact subtitle, and assert.equal(presentation.href, "/conversations/folders/mac-studio%3Aboss"); }); +test("folder archive search aliases stay bounded to the latest thread titles", async () => { + await setup(); + const state = await readState(); + + state.projects = state.projects.filter((project) => project.id === "master-agent"); + state.projects.push( + buildImportedThreadProject("mac-studio", "boss-thread-1", "Boss", "boss", "发布回滚", "thread-1", "2026-03-30T14:00:00+08:00"), + buildImportedThreadProject("mac-studio", "boss-thread-2", "Boss", "boss", "Android UI 收尾", "thread-2", "2026-03-30T13:00:00+08:00"), + buildImportedThreadProject("mac-studio", "boss-thread-3", "Boss", "boss", "日志收口", "thread-3", "2026-03-30T12:00:00+08:00"), + buildImportedThreadProject("mac-studio", "boss-thread-4", "Boss", "boss", "网络修复", "thread-4", "2026-03-30T11:00:00+08:00"), + buildImportedThreadProject("mac-studio", "boss-thread-5", "Boss", "boss", "审阅确认", "thread-5", "2026-03-30T10:00:00+08:00"), + ); + + const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive"); + + assert.ok(folder, "expected grouped folder archive item"); + assert.deepEqual(folder?.searchAliases, ["发布回滚", "Android UI 收尾", "日志收口", "网络修复"]); +}); + test("conversation items expose context status while keeping idle activity silent", async () => { await setup(); const state = await readState();