Skip duplicate chat payloads and batch layouts
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user