Compact imported single-thread conversation copy

This commit is contained in:
kris
2026-04-07 12:49:53 +08:00
parent c5223c7c16
commit a43bb92f3c
4 changed files with 122 additions and 8 deletions

View File

@@ -59,7 +59,9 @@ public final class WechatSurfaceMapper {
JSONObject avatar = source.optJSONObject("avatar");
boolean isGroup = source.optBoolean("isGroup", groupAvatarMembers.size() > 1);
String conversationType = source.optString("conversationType", "");
String threadTitle = source.optString("threadTitle", source.optString("title", source.optString("projectTitle", "")));
String threadTitle = trimLocalWorkspacePrefix(
source.optString("threadTitle", source.optString("title", source.optString("projectTitle", "")))
);
if ("folder_archive".equals(conversationType)) {
threadTitle = source.optString(
"projectTitle",
@@ -70,7 +72,7 @@ public final class WechatSurfaceMapper {
return new ConversationRow(
threadTitle,
source.optString("folderLabel", ""),
source.optString("lastMessagePreview", source.optString("preview", "")),
compactImportedThreadPreview(source.optString("lastMessagePreview", source.optString("preview", ""))),
source.optString("timeLabel", source.optString("latestReplyLabel", "")),
source.optInt("unreadCount", 0),
pinnedLabel,
@@ -408,7 +410,7 @@ public final class WechatSurfaceMapper {
);
putIfNotEmpty(folder, "projectTitle", projectTitle);
putIfNotEmpty(folder, "threadTitle", projectTitle);
String recentThread = latest.optString("threadTitle", "").trim();
String recentThread = trimLocalWorkspacePrefix(latest.optString("threadTitle", "").trim());
putIfNotEmpty(
folder,
"folderLabel",
@@ -422,11 +424,16 @@ public final class WechatSurfaceMapper {
safePut(folder, "searchAliases", searchAliases);
safePut(folder, "searchTargetProjectIds", searchTargetProjectIds);
}
putIfNotEmpty(folder, "preview", firstNonBlank(latest.optString("preview", ""), latest.optString("lastMessagePreview", "")));
String compactPreview = compactImportedThreadPreview(
firstNonBlank(latest.optString("preview", ""), latest.optString("lastMessagePreview", ""))
);
putIfNotEmpty(folder, "preview", compactPreview);
putIfNotEmpty(
folder,
"lastMessagePreview",
firstNonBlank(latest.optString("lastMessagePreview", ""), latest.optString("preview", ""))
compactImportedThreadPreview(
firstNonBlank(latest.optString("lastMessagePreview", ""), latest.optString("preview", ""))
)
);
safePut(folder, "activityIconCount", Math.max(0, Math.min(4, sumInt(items, "activityIconCount"))));
if (hasPinnedLabel(items)) {
@@ -637,6 +644,26 @@ public final class WechatSurfaceMapper {
}
}
private static String compactImportedThreadPreview(String value) {
String preview = value == null ? "" : value.trim();
if (preview.matches("^已从设备.+导入线程《.+》[。.]?$")) {
return "已导入线程";
}
return preview;
}
private static String trimLocalWorkspacePrefix(String value) {
String label = value == null ? "" : value.trim();
if (label.isEmpty()) {
return "";
}
String normalized = label.replace('\\', '/');
return normalized
.replaceFirst("^/Users/[^/]+/code/", "")
.replaceFirst("^/home/[^/]+/code/", "")
.replaceFirst("^[A-Za-z]:/Users/[^/]+/code/", "");
}
private static String firstNonBlank(String... values) {
if (values == null) {
return "";

View File

@@ -1,6 +1,9 @@
package com.hyzq.boss;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -12,6 +15,8 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class WechatSurfaceMapperTest {
@Test
public void toConversationRow_mapsWechatConversationFieldsFromThreadPayload() throws Exception {
@@ -79,6 +84,59 @@ public class WechatSurfaceMapperTest {
assertEquals("", row.avatarPrimary);
}
@Test
public void normalizeConversationHomeFeed_compactsImportedFolderPreviewAndTrimsWorkspacePrefix() throws Exception {
JSONArray source = new JSONArray()
.put(new JSONObject()
.put("conversationType", "single_device")
.put("projectId", "wenshen-thread-1")
.put("folderKey", "mac-studio:/users/kris/code/wenshenapp")
.put("projectTitle", "wenshenapp")
.put("threadTitle", "/Users/kris/code/wenshenapp/docs/superpowers/specs/2026-04-06-context-cleanup-design.md")
.put("folderLabel", "wenshenapp")
.put("preview", "已从设备 Mac Studio 导入线程《wenshenapp · 019d60》。")
.put("lastMessagePreview", "已从设备 Mac Studio 导入线程《wenshenapp · 019d60》。")
.put("latestReplyAt", "2026-04-06T17:35:00+08:00")
.put("latestReplyLabel", "17:35"))
.put(new JSONObject()
.put("conversationType", "single_device")
.put("projectId", "wenshen-thread-2")
.put("folderKey", "mac-studio:/users/kris/code/wenshenapp")
.put("projectTitle", "wenshenapp")
.put("threadTitle", "补齐 Android 会话可读性")
.put("folderLabel", "wenshenapp")
.put("preview", "最近消息:补齐 Android 会话可读性")
.put("lastMessagePreview", "最近消息:补齐 Android 会话可读性")
.put("latestReplyAt", "2026-04-06T16:30:00+08:00")
.put("latestReplyLabel", "16:30"));
JSONArray normalized = WechatSurfaceMapper.normalizeConversationHomeFeed(source);
JSONObject folder = normalized.optJSONObject(0);
assertNotNull(folder);
assertEquals("folder_archive", folder.optString("conversationType", ""));
assertEquals("2 个线程 · 最近wenshenapp/docs/superpowers/specs/2026-04-06-context-cleanup-design.md", folder.optString("folderLabel", ""));
assertEquals("已导入线程", folder.optString("preview", ""));
assertEquals("已导入线程", folder.optString("lastMessagePreview", ""));
}
@Test
public void toConversationRow_compactsImportedPreviewAndTrimsWorkspacePrefixForSingleThread() throws Exception {
JSONObject item = new JSONObject()
.put("conversationType", "single_device")
.put("projectTitle", "zhanglaoshi")
.put("threadTitle", "/Users/kris/code/zhanglaoshi/docs/superpowers/specs/2026-04-06-single-thread-cleanup-design.md")
.put("folderLabel", "zhanglaoshi")
.put("preview", "已从设备 Mac Studio 导入线程《zhanglaoshi · 019d61》。")
.put("lastMessagePreview", "已从设备 Mac Studio 导入线程《zhanglaoshi · 019d61》。")
.put("latestReplyLabel", "17:35");
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
assertEquals("zhanglaoshi/docs/superpowers/specs/2026-04-06-single-thread-cleanup-design.md", row.threadTitle);
assertEquals("已导入线程", row.lastMessagePreview);
}
@Test
public void toDeviceRow_mapsLegacyWechatThreeLineSummary() throws Exception {
JSONObject item = new StubJSONObject()

View File

@@ -355,11 +355,12 @@ function buildConversationItem(state: BossState, project: Project): Conversation
const devices = state.devices.filter((device) => project.deviceIds.includes(device.id));
const threadViews = threadViewsForProject(state, project.id);
const topThread = threadViews[0]?.snapshot;
const threadTitle = project.threadMeta?.threadDisplayName ?? project.name;
const threadTitle = trimLocalWorkspacePrefix(project.threadMeta?.threadDisplayName ?? project.name);
const folderLabel = project.threadMeta?.folderName ?? "";
const activityIconCount = deriveConversationActivityIconCount(state, project);
const topPinnedLabel = isTopPinnedConversation(project) ? "置顶" : undefined;
const latestConversationActivityAt = deriveLatestConversationActivityAt(project);
const compactPreview = compactImportedThreadPreview(project.preview);
const groupMembers = project.isGroup
? project.groupMembers.map((member) => ({
threadId: member.threadId,
@@ -379,8 +380,8 @@ function buildConversationItem(state: BossState, project: Project): Conversation
threadTitle,
folderLabel,
folderKey: buildFolderKey(project),
preview: project.preview,
lastMessagePreview: project.preview,
preview: compactPreview,
lastMessagePreview: compactPreview,
activityIconCount,
topPinnedLabel,
manualPinned: Boolean(project.pinned && !project.systemPinned),

View File

@@ -581,6 +581,34 @@ test("conversation home compacts imported previews and trims local workspace pre
assert.equal(folder?.lastMessagePreview, "已导入线程");
});
test("conversation home compacts imported previews and trims local workspace prefixes for single-thread items", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"zhanglaoshi-thread-1",
"zhanglaoshi",
"/Users/kris/code/zhanglaoshi",
"/Users/kris/code/zhanglaoshi/docs/superpowers/specs/2026-04-06-single-thread-cleanup-design.md",
"thread-1",
"2026-04-06T15:00:00+08:00",
),
preview: "已从设备 Mac Studio 导入线程《zhanglaoshi · 019d61》。",
lastMessageAt: "2026-04-06T15:00:00+08:00",
});
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "zhanglaoshi-thread-1");
assert.ok(item, "expected single imported thread item");
assert.equal(item?.conversationType, "single_device");
assert.equal(item?.threadTitle, "zhanglaoshi/docs/superpowers/specs/2026-04-06-single-thread-cleanup-design.md");
assert.equal(item?.preview, "已导入线程");
assert.equal(item?.lastMessagePreview, "已导入线程");
});
test("folder archive homepage rows expose pin toggles when the folder is pinned", async () => {
await setup();
const state = await readState();