Cache markdown and append realtime messages

This commit is contained in:
kris
2026-04-10 22:40:49 +08:00
parent 1b0f126d4f
commit 05dc9d8788
4 changed files with 161 additions and 1 deletions

View File

@@ -1,10 +1,12 @@
package com.hyzq.boss;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
@@ -14,6 +16,7 @@ import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.LruCache;
import java.util.ArrayList;
import java.util.List;
@@ -25,6 +28,7 @@ public final class BossMarkdown {
private static final Pattern BULLET_PATTERN = Pattern.compile("^[-*]\\s+(.+)$");
private static final Pattern ORDERED_PATTERN = Pattern.compile("^(\\d+)\\.\\s+(.+)$");
private static final Pattern INLINE_TOKEN_PATTERN = Pattern.compile("(\\*\\*([^*]+)\\*\\*)|(`([^`]+)`)");;
private static final LruCache<String, CharSequence> RENDER_CACHE = new LruCache<>(180);
private BossMarkdown() {}
@@ -32,6 +36,11 @@ public final class BossMarkdown {
if (TextUtils.isEmpty(markdown) || TextUtils.isEmpty(markdown.trim())) {
return "(空消息)";
}
String cacheKey = buildCacheKey(context, markdown, outgoing);
CharSequence cached = RENDER_CACHE.get(cacheKey);
if (cached != null) {
return cached;
}
Palette palette = Palette.resolve(context, outgoing);
SpannableStringBuilder builder = new SpannableStringBuilder();
String normalized = markdown.replace("\r\n", "\n").replace('\r', '\n');
@@ -90,7 +99,14 @@ public final class BossMarkdown {
}
trimTrailingNewline(builder);
return builder;
CharSequence rendered = SpannedString.valueOf(builder);
RENDER_CACHE.put(cacheKey, rendered);
return rendered;
}
private static String buildCacheKey(Context context, String markdown, boolean outgoing) {
int uiMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return (outgoing ? "out" : "in") + "|" + uiMode + "|" + markdown;
}
private static void appendHeading(SpannableStringBuilder builder, String text, int level, Palette palette) {

View File

@@ -80,6 +80,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private @Nullable JSONObject currentPendingDispatchPlan;
private @Nullable JSONObject currentRejectedDispatchPlan;
private @Nullable JSONObject currentParticipantsPayload;
private @Nullable JSONObject currentRenderedProjectPayload;
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
private ActivityResultLauncher<Intent> conversationInfoLauncher;
private ActivityResultLauncher<Intent> masterAgentPromptLauncher;
@@ -370,6 +371,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (projectMessagesPayload == null) {
return false;
}
if (tryAppendRealtimeMessagesPatch(projectMessagesPayload)) {
return true;
}
runOnUiThread(() -> {
if (reloadInFlight) {
scheduleRealtimeReload(false);
@@ -382,6 +386,71 @@ public class ProjectDetailActivity extends BossScreenActivity {
return true;
}
private boolean tryAppendRealtimeMessagesPatch(JSONObject projectMessagesPayload) {
if (currentRenderedProjectPayload == null
|| contentLayout == null
|| pendingOutgoingBubble != null
|| masterAgentReplyWaiting
|| masterAgentReplyTimedOut
|| (selectionState != null && selectionState.multiSelecting)) {
return false;
}
JSONObject currentProject = currentRenderedProjectPayload.optJSONObject("project");
JSONObject nextProject = projectMessagesPayload.optJSONObject("project");
if (currentProject == null || nextProject == null) {
return false;
}
JSONArray currentMessages = currentProject.optJSONArray("messages");
JSONArray nextMessages = nextProject.optJSONArray("messages");
if (currentMessages == null || nextMessages == null) {
return false;
}
List<String> currentIds = collectMessageIds(currentMessages);
List<String> nextIds = collectMessageIds(nextMessages);
if (currentIds.isEmpty() || nextIds.size() <= currentIds.size()) {
return false;
}
for (int i = 0; i < currentIds.size(); i++) {
if (!currentIds.get(i).equals(nextIds.get(i))) {
return false;
}
}
runOnUiThread(() -> {
if (currentRenderedProjectPayload == null || contentLayout == null) {
renderLoadedProjectSnapshot(new ProjectSnapshot(projectMessagesPayload, null, null));
return;
}
JSONObject project = projectMessagesPayload.optJSONObject("project");
JSONArray devices = projectMessagesPayload.optJSONArray("devices");
JSONObject threadMeta = project == null ? null : project.optJSONObject("threadMeta");
String title = project != null ? project.optString("name", "项目详情") : "项目详情";
initialProjectName = title;
projectIsGroup = project != null && project.optBoolean("isGroup", false);
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
projectCollaborationMode = project == null ? "development" : project.optString("collaborationMode", projectCollaborationMode);
projectApprovalState = project == null ? "not_required" : project.optString("approvalState", projectApprovalState);
lightDispatchReminderEnabled = project != null && project.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
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;
}
appendContent(buildMessageView(message));
}
currentRenderedProjectPayload = copyJson(projectMessagesPayload);
setRefreshing(false);
updateSelectionUi();
if (ProjectChatUiState.shouldAutoScroll(renderNearBottom, false)) {
scrollChatToBottom();
}
});
return true;
}
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
pruneRecentRealtimeEvents(now);
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
@@ -609,6 +678,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
}
currentRenderedProjectPayload = copyJson(payload);
setRefreshing(false);
updateSelectionUi();
if (ProjectChatUiState.shouldAutoScroll(renderNearBottom, renderForcedScrollToBottom)) {
@@ -2598,6 +2668,17 @@ public class ProjectDetailActivity extends BossScreenActivity {
return ids;
}
private JSONObject copyJson(@Nullable JSONObject source) {
if (source == null) {
return new JSONObject();
}
try {
return new JSONObject(source.toString());
} catch (org.json.JSONException ignored) {
return new JSONObject();
}
}
@Nullable
private String labelForMessageKind(String kind) {
if (TextUtils.isEmpty(kind) || "text".equals(kind)) {