From 164a7568a7b1f9cab5033645b858b37d8df4974f Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 10 Apr 2026 22:45:19 +0800 Subject: [PATCH] Skip duplicate chat payloads and batch layouts --- .../com/hyzq/boss/ProjectDetailActivity.java | 130 ++++++++++++------ ...d-chat-incremental-realtime-append.test.ts | 30 ++++ 2 files changed, 120 insertions(+), 40 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java index 5d3c618..a2a5eac 100644 --- a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java @@ -371,6 +371,9 @@ public class ProjectDetailActivity extends BossScreenActivity { if (projectMessagesPayload == null) { return false; } + if (trySkipUnchangedRealtimeMessagesPatch(projectMessagesPayload)) { + return true; + } if (tryAppendRealtimeMessagesPatch(projectMessagesPayload)) { return true; } @@ -386,6 +389,33 @@ public class ProjectDetailActivity extends BossScreenActivity { return true; } + private boolean trySkipUnchangedRealtimeMessagesPatch(JSONObject projectMessagesPayload) { + if (currentRenderedProjectPayload == null || projectMessagesPayload == null) { + return false; + } + JSONObject currentProject = currentRenderedProjectPayload.optJSONObject("project"); + JSONObject nextProject = projectMessagesPayload.optJSONObject("project"); + if (currentProject == null || nextProject == null) { + return false; + } + if (!TextUtils.equals( + currentProject.optString("id", "").trim(), + nextProject.optString("id", "").trim() + )) { + return false; + } + JSONArray currentMessages = currentProject.optJSONArray("messages"); + JSONArray nextMessages = nextProject.optJSONArray("messages"); + if (currentMessages == null || nextMessages == null) { + return false; + } + if (!TextUtils.equals(currentMessages.toString(), nextMessages.toString())) { + return false; + } + currentRenderedProjectPayload = copyJson(projectMessagesPayload); + return true; + } + private boolean tryAppendRealtimeMessagesPatch(JSONObject projectMessagesPayload) { if (currentRenderedProjectPayload == null || contentLayout == null @@ -434,13 +464,15 @@ public class ProjectDetailActivity extends BossScreenActivity { selectionState = ProjectChatUiState.reconcileSelection(selectionState, nextIds); renderNearBottom = isChatNearBottom(); - for (int i = currentIds.size(); i < nextMessages.length(); i++) { - JSONObject message = nextMessages.optJSONObject(i); - if (message == null) { - continue; + runWithSuppressedContentLayout(() -> { + for (int i = currentIds.size(); i < nextMessages.length(); i++) { + JSONObject message = nextMessages.optJSONObject(i); + if (message == null) { + continue; + } + appendContent(buildMessageView(message)); } - appendContent(buildMessageView(message)); - } + }); currentRenderedProjectPayload = copyJson(projectMessagesPayload); setRefreshing(false); updateSelectionUi(); @@ -638,45 +670,47 @@ public class ProjectDetailActivity extends BossScreenActivity { updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices)); renderQuickActions(); - replaceContent(); - pendingOutgoingBubble = null; - if (currentPendingDispatchPlan != null) { - appendContent(buildPendingDispatchPlanView(currentPendingDispatchPlan)); - } else if (projectIsGroup && "rejected".equals(projectApprovalState) && currentRejectedDispatchPlan != null) { - appendContent(buildRejectedDispatchPlanView(currentRejectedDispatchPlan)); - } - if (projectIsGroup - && effectiveParticipantsPayload != null - && effectiveParticipantsPayload.optBoolean("repairRequired", false)) { - appendContent(buildRepairGroupMembersView(effectiveParticipantsPayload)); - } - JSONArray messages = project == null ? null : project.optJSONArray("messages"); selectionState = ProjectChatUiState.reconcileSelection(selectionState, collectMessageIds(messages)); - if (messages != null && messages.length() > 0) { - for (int i = 0; i < messages.length(); i++) { - JSONObject message = messages.optJSONObject(i); - if (message == null) { - continue; - } - appendContent(buildMessageView(message)); + runWithSuppressedContentLayout(() -> { + replaceContent(); + pendingOutgoingBubble = null; + if (currentPendingDispatchPlan != null) { + appendContent(buildPendingDispatchPlanView(currentPendingDispatchPlan)); + } else if (projectIsGroup && "rejected".equals(projectApprovalState) && currentRejectedDispatchPlan != null) { + appendContent(buildRejectedDispatchPlanView(currentRejectedDispatchPlan)); + } + if (projectIsGroup + && effectiveParticipantsPayload != null + && effectiveParticipantsPayload.optBoolean("repairRequired", false)) { + appendContent(buildRepairGroupMembersView(effectiveParticipantsPayload)); } - } else { - appendContent(BossUi.buildMessagePlaceholder(this, "还没有项目消息,先发一条开始对话。")); - } - boolean masterAgentHasReply = isMasterAgentConversation() - && ProjectChatUiState.hasReplyBeyondBaseline(project, masterAgentReplyBaselineMessageId); - if (masterAgentHasReply) { - clearMasterAgentReplyState(); - } - if (isMasterAgentConversation()) { - if (masterAgentReplyWaiting) { - appendContent(buildMasterAgentReplyStateView(false)); - } else if (masterAgentReplyTimedOut) { - appendContent(buildMasterAgentReplyStateView(true)); + if (messages != null && messages.length() > 0) { + for (int i = 0; i < messages.length(); i++) { + JSONObject message = messages.optJSONObject(i); + if (message == null) { + continue; + } + appendContent(buildMessageView(message)); + } + } else { + appendContent(BossUi.buildMessagePlaceholder(this, "还没有项目消息,先发一条开始对话。")); } - } + + boolean masterAgentHasReply = isMasterAgentConversation() + && ProjectChatUiState.hasReplyBeyondBaseline(project, masterAgentReplyBaselineMessageId); + if (masterAgentHasReply) { + clearMasterAgentReplyState(); + } + if (isMasterAgentConversation()) { + if (masterAgentReplyWaiting) { + appendContent(buildMasterAgentReplyStateView(false)); + } else if (masterAgentReplyTimedOut) { + appendContent(buildMasterAgentReplyStateView(true)); + } + } + }); currentRenderedProjectPayload = copyJson(payload); setRefreshing(false); @@ -686,6 +720,22 @@ public class ProjectDetailActivity extends BossScreenActivity { } } + private void runWithSuppressedContentLayout(Runnable action) { + if (action == null) { + return; + } + if (contentLayout == null) { + action.run(); + return; + } + contentLayout.suppressLayout(true); + try { + action.run(); + } finally { + contentLayout.suppressLayout(false); + } + } + private void updateRealtimeSubscription() { if (apiClient != null && apiClient.hasSessionHints()) { realtimeClient.start(); diff --git a/tests/android-chat-incremental-realtime-append.test.ts b/tests/android-chat-incremental-realtime-append.test.ts index 3e238fe..7f88401 100644 --- a/tests/android-chat-incremental-realtime-append.test.ts +++ b/tests/android-chat-incremental-realtime-append.test.ts @@ -29,4 +29,34 @@ test("ProjectDetailActivity keeps a rendered project snapshot for append-only re /appendContent\(buildMessageView\(message\)\);/, "expected append-only realtime patches to add only the new message views", ); + assert.match( + source, + /if \(trySkipUnchangedRealtimeMessagesPatch\(projectMessagesPayload\)\) \{\s*return true;\s*\}/, + "expected chat page to skip duplicate realtime message payloads before rerendering", + ); + assert.match( + source, + /private boolean trySkipUnchangedRealtimeMessagesPatch\(JSONObject projectMessagesPayload\)/, + "expected chat page to expose a duplicate-payload fast path", + ); +}); + +test("ProjectDetailActivity suppresses intermediate layouts while rebuilding or appending chat content", async () => { + const source = await readSource("../android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java"); + + assert.match( + source, + /private void runWithSuppressedContentLayout\(Runnable action\)/, + "expected chat page to centralize layout suppression for bulk content updates", + ); + assert.match( + source, + /contentLayout\.suppressLayout\(true\);/, + "expected chat page to pause intermediate layout passes during bulk updates", + ); + assert.match( + source, + /contentLayout\.suppressLayout\(false\);/, + "expected chat page to resume layout after bulk updates", + ); });