feat: surface thread status summary in conversation info
This commit is contained in:
@@ -50,7 +50,17 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json));
|
||||
JSONObject threadStatusPayload = null;
|
||||
try {
|
||||
BossApiClient.ApiResponse threadStatusResponse = apiClient.getThreadStatus(projectId);
|
||||
if (threadStatusResponse.ok()) {
|
||||
threadStatusPayload = threadStatusResponse.json;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
threadStatusPayload = null;
|
||||
}
|
||||
JSONObject finalThreadStatusPayload = threadStatusPayload;
|
||||
runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json, finalThreadStatusPayload));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -60,7 +70,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void renderConversation(JSONObject detail, JSONObject participantsPayload) {
|
||||
private void renderConversation(JSONObject detail, JSONObject participantsPayload, @Nullable JSONObject threadStatusPayload) {
|
||||
replaceContent();
|
||||
JSONObject project = detail.optJSONObject("project");
|
||||
JSONArray participants = participantsPayload.optJSONArray("participants");
|
||||
@@ -84,6 +94,8 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
buildHeaderDetail(project, threadMeta, participantCount)
|
||||
));
|
||||
|
||||
appendThreadStatusSummary(threadStatusPayload);
|
||||
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"发起群聊",
|
||||
@@ -140,6 +152,75 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private void appendThreadStatusSummary(@Nullable JSONObject threadStatusPayload) {
|
||||
if (threadStatusPayload == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject document = threadStatusPayload.optJSONObject("threadStatusDocument");
|
||||
if (document == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray recentProgressEvents = threadStatusPayload.optJSONArray("recentProgressEvents");
|
||||
int eventCount = recentProgressEvents == null ? 0 : recentProgressEvents.length();
|
||||
String body = buildThreadStatusSummaryBody(document, eventCount);
|
||||
String meta = buildThreadStatusSummaryMeta(document, eventCount);
|
||||
appendContent(BossUi.buildCard(this, "线程状态摘要", body, meta));
|
||||
}
|
||||
|
||||
private String buildThreadStatusSummaryBody(JSONObject document, int eventCount) {
|
||||
return joinNonEmptyLines(
|
||||
formatSummaryLine("当前目标", document.optString("projectGoal", "")),
|
||||
formatSummaryLine("当前进度", document.optString("currentProgress", "")),
|
||||
formatSummaryLine("当前阻塞", document.optString("currentBlockers", "")),
|
||||
formatSummaryLine("建议下一步", document.optString("recommendedNextStep", "")),
|
||||
eventCount > 0 ? "最近进展:" + eventCount + " 条" : ""
|
||||
);
|
||||
}
|
||||
|
||||
private String buildThreadStatusSummaryMeta(JSONObject document, int eventCount) {
|
||||
return joinNonEmptyParts(
|
||||
projectFolderName,
|
||||
eventCount > 0 ? "最近 " + eventCount + " 条进展" : "暂无进展",
|
||||
document.optString("updatedAt", "").isEmpty() ? "" : "更新于 " + document.optString("updatedAt", "")
|
||||
);
|
||||
}
|
||||
|
||||
private String formatSummaryLine(String label, String value) {
|
||||
String trimmed = value == null ? "" : value.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return label + ":" + trimmed;
|
||||
}
|
||||
|
||||
private String joinNonEmptyLines(String... values) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String value : values) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append('\n');
|
||||
}
|
||||
builder.append(value.trim());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String joinNonEmptyParts(String... values) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String value : values) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(value.trim());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private LinearLayout buildParticipantRow(JSONObject participant) {
|
||||
boolean sourceProject = participant.optBoolean("isSourceProject", false);
|
||||
String participantProjectId = participant.optString("projectId", "");
|
||||
|
||||
@@ -43,16 +43,20 @@ public class ConversationInfoActivityTest {
|
||||
activity,
|
||||
"renderConversation",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildThreadStatusPayload())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(0), "单线程会话"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "发起群聊"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程加入新群"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(2), "线程详情"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(2), "查看当前线程聊天与项目"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "线程状态摘要"));
|
||||
assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "当前进度:已经记录最近 2 条进展"));
|
||||
assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "建议下一步:继续同步 Android 只读页"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(2), "发起群聊"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(2), "选择其他线程加入新群"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(3), "线程详情"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(3), "查看当前线程聊天与项目"));
|
||||
assertTrue(viewTreeContainsText(content, "参与线程"));
|
||||
assertTrue(viewTreeContainsText(content, "硬件审计协作"));
|
||||
assertFalse(viewTreeContainsText(content, "从当前会话选择其他线程,创建新的独立群聊"));
|
||||
@@ -73,7 +77,8 @@ public class ConversationInfoActivityTest {
|
||||
activity,
|
||||
"renderConversation",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildThreadStatusPayload())
|
||||
);
|
||||
|
||||
View threadDetailRow = findClickableViewContainingText(
|
||||
@@ -114,7 +119,8 @@ public class ConversationInfoActivityTest {
|
||||
activity,
|
||||
"renderConversation",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildThreadStatusPayload())
|
||||
);
|
||||
|
||||
View threadStatusRow = findClickableViewContainingText(
|
||||
@@ -188,6 +194,19 @@ public class ConversationInfoActivityTest {
|
||||
return new JSONObject().put("participants", participants);
|
||||
}
|
||||
|
||||
private static JSONObject buildThreadStatusPayload() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("threadStatusDocument", new JSONObject()
|
||||
.put("projectGoal", "完成线程状态回归")
|
||||
.put("currentProgress", "已经记录最近 2 条进展")
|
||||
.put("currentBlockers", "暂无阻塞")
|
||||
.put("recommendedNextStep", "继续同步 Android 只读页")
|
||||
.put("updatedAt", "2026-04-04T18:00:00+08:00"))
|
||||
.put("recentProgressEvents", new JSONArray()
|
||||
.put(new JSONObject().put("summary", "事件 2"))
|
||||
.put(new JSONObject().put("summary", "事件 1")));
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
@@ -207,6 +226,25 @@ public class ConversationInfoActivityTest {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsTextFragment(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (text != null && text.toString().contains(expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsTextFragment(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static View findClickableViewContainingText(View root, String expectedText) {
|
||||
if (root == null) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user