From d2291af32c6480bd865ead8568dacc8596552e4b Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 28 Mar 2026 08:45:20 +0800 Subject: [PATCH] fix: stabilize native chat selection chrome --- .../com/hyzq/boss/ProjectChatUiState.java | 109 ++++++++++++++++++ .../com/hyzq/boss/ProjectDetailActivity.java | 74 +++++++----- .../com/hyzq/boss/ProjectChatUiStateTest.java | 60 ++++++++++ 3 files changed, 215 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java b/android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java index 245caf0..e182136 100644 --- a/android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java +++ b/android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java @@ -2,8 +2,10 @@ package com.hyzq.boss; import androidx.annotation.Nullable; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; public final class ProjectChatUiState { @@ -20,6 +22,40 @@ public final class ProjectChatUiState { } } + public static final class ChromeState { + public final boolean multiSelecting; + public final boolean showComposer; + public final boolean showMultiSelectBar; + public final boolean showRefresh; + public final boolean showHeaderAction; + public final boolean forwardEnabled; + public final String backLabel; + public final String title; + public final String subtitle; + + private ChromeState( + boolean multiSelecting, + boolean showComposer, + boolean showMultiSelectBar, + boolean showRefresh, + boolean showHeaderAction, + boolean forwardEnabled, + String backLabel, + String title, + String subtitle + ) { + this.multiSelecting = multiSelecting; + this.showComposer = showComposer; + this.showMultiSelectBar = showMultiSelectBar; + this.showRefresh = showRefresh; + this.showHeaderAction = showHeaderAction; + this.forwardEnabled = forwardEnabled; + this.backLabel = backLabel; + this.title = title; + this.subtitle = subtitle; + } + } + public static boolean canSend(String text, boolean sending) { return !sending && text != null && !text.trim().isEmpty(); } @@ -54,6 +90,57 @@ public final class ProjectChatUiState { return state != null && state.multiSelecting && state.selectedMessageIds.size() >= 2; } + public static SelectionState reconcileSelection( + @Nullable SelectionState current, + @Nullable List availableMessageIds + ) { + if (current == null || current.selectedMessageIds.isEmpty() || availableMessageIds == null || availableMessageIds.isEmpty()) { + return emptySelection(); + } + LinkedHashSet available = new LinkedHashSet<>(availableMessageIds); + LinkedHashSet selected = new LinkedHashSet<>(); + for (String selectedMessageId : current.selectedMessageIds) { + if (available.contains(selectedMessageId)) { + selected.add(selectedMessageId); + } + } + return new SelectionState(selected); + } + + public static ChromeState resolveChromeState( + @Nullable SelectionState selectionState, + boolean conversationInfoReady, + @Nullable String defaultTitle, + @Nullable String defaultSubtitle + ) { + boolean multiSelecting = selectionState != null && selectionState.multiSelecting; + if (multiSelecting) { + int selectedCount = selectionState.selectedMessageIds.size(); + return new ChromeState( + true, + false, + true, + false, + false, + canForwardSelection(selectionState), + "取消", + "已选 " + selectedCount + " 条", + "选择要转发的消息" + ); + } + return new ChromeState( + false, + true, + false, + true, + conversationInfoReady, + false, + "返回", + isBlank(defaultTitle) ? "项目详情" : defaultTitle, + isBlank(defaultSubtitle) ? "原生页面" : defaultSubtitle + ); + } + @Nullable public static String labelForForwardKind(@Nullable String kind) { if ("forward_single".equals(kind)) { @@ -64,4 +151,26 @@ public final class ProjectChatUiState { } return null; } + + public static String summarizeForwardBundle(@Nullable String lastBody, int itemCount) { + if (itemCount > 0 && !isBlank(lastBody)) { + return itemCount + " 条消息 · 最后一条:" + truncate(lastBody, 28); + } + if (itemCount > 0) { + return itemCount + " 条消息"; + } + return truncate(lastBody, 28); + } + + private static boolean isBlank(@Nullable String value) { + return value == null || value.trim().isEmpty(); + } + + private static String truncate(@Nullable String value, int maxLength) { + String normalized = value == null ? "" : value.trim(); + if (normalized.length() <= maxLength) { + return normalized; + } + return normalized.substring(0, maxLength) + "…"; + } } 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 0e1fc80..1916277 100644 --- a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java @@ -184,6 +184,7 @@ public class ProjectDetailActivity extends BossScreenActivity { pendingOutgoingBubble = null; 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); @@ -460,38 +461,39 @@ public class ProjectDetailActivity extends BossScreenActivity { } private void updateSelectionUi() { - boolean multiSelecting = selectionState != null && selectionState.multiSelecting; + ProjectChatUiState.ChromeState chromeState = ProjectChatUiState.resolveChromeState( + selectionState, + conversationInfoReady, + currentScreenTitle, + currentScreenSubtitle + ); if (composerRow != null) { - composerRow.setVisibility(multiSelecting ? View.GONE : View.VISIBLE); + composerRow.setVisibility(chromeState.showComposer ? View.VISIBLE : View.GONE); } if (multiSelectActionsLayout != null) { - multiSelectActionsLayout.setVisibility(multiSelecting ? View.VISIBLE : View.GONE); + multiSelectActionsLayout.setVisibility(chromeState.showMultiSelectBar ? View.VISIBLE : View.GONE); } if (multiSelectForwardButton != null) { - multiSelectForwardButton.setEnabled(!isComposerBusy() && ProjectChatUiState.canForwardSelection(selectionState)); + multiSelectForwardButton.setEnabled(!isComposerBusy() && chromeState.forwardEnabled); } if (refreshLayout != null) { - refreshLayout.setEnabled(!multiSelecting); + refreshLayout.setEnabled(!chromeState.multiSelecting); } - - if (multiSelecting) { - backButton.setText("取消"); - backButton.setOnClickListener(v -> exitMultiSelect()); - refreshButton.setVisibility(View.GONE); - hideHeaderAction(); - titleView.setText("已选 " + selectionState.selectedMessageIds.size() + " 条"); - subtitleView.setText("选择要转发的消息"); - } else { - backButton.setText("返回"); - backButton.setOnClickListener(v -> finish()); - refreshButton.setVisibility(View.VISIBLE); - titleView.setText(TextUtils.isEmpty(currentScreenTitle) ? "项目详情" : currentScreenTitle); - subtitleView.setText(TextUtils.isEmpty(currentScreenSubtitle) ? "原生页面" : currentScreenSubtitle); - if (conversationInfoReady) { - setHeaderAction(WechatSurfaceMapper.conversationInfoActionLabel(), v -> openConversationInfo()); - } else { - hideHeaderAction(); + backButton.setText(chromeState.backLabel); + backButton.setOnClickListener(v -> { + if (chromeState.multiSelecting) { + exitMultiSelect(); + return; } + finish(); + }); + refreshButton.setVisibility(chromeState.showRefresh ? View.VISIBLE : View.GONE); + titleView.setText(chromeState.title); + subtitleView.setText(chromeState.subtitle); + if (chromeState.showHeaderAction) { + setHeaderAction(WechatSurfaceMapper.conversationInfoActionLabel(), v -> openConversationInfo()); + } else { + hideHeaderAction(); } refreshMessageSelectionViews(); } @@ -654,15 +656,31 @@ public class ProjectDetailActivity extends BossScreenActivity { JSONArray items = forwardBundle.optJSONArray("items"); JSONObject lastItem = items == null || items.length() == 0 ? null : items.optJSONObject(items.length() - 1); String lastBody = lastItem == null ? "" : lastItem.optString("body", ""); - if (itemCount > 0 && !TextUtils.isEmpty(lastBody)) { - return itemCount + " 条消息 · 最后一条:" + lastBody; - } - if (itemCount > 0) { - return itemCount + " 条消息"; + String summarized = ProjectChatUiState.summarizeForwardBundle(lastBody, itemCount); + if (!TextUtils.isEmpty(summarized)) { + return summarized; } return message.optString("body", "转发的聊天记录"); } + private List collectMessageIds(@Nullable JSONArray messages) { + ArrayList ids = new ArrayList<>(); + if (messages == null) { + return ids; + } + for (int i = 0; i < messages.length(); i++) { + JSONObject message = messages.optJSONObject(i); + if (message == null) { + continue; + } + String messageId = message.optString("id", ""); + if (!TextUtils.isEmpty(messageId)) { + ids.add(messageId); + } + } + return ids; + } + @Nullable private String labelForMessageKind(String kind) { if (TextUtils.isEmpty(kind) || "text".equals(kind)) { diff --git a/android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java b/android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java index 8bb080a..36020c5 100644 --- a/android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; +import java.util.List; import org.junit.Test; @@ -82,4 +83,63 @@ public class ProjectChatUiStateTest { public void bundleForwardMessageUsesBundleModeLabel() { assertEquals("聊天记录", ProjectChatUiState.labelForForwardKind("forward_bundle")); } + + @Test + public void chromeStateUsesMultiSelectHeaderAndActionsWhenSelecting() { + ProjectChatUiState.SelectionState state = ProjectChatUiState.toggleSelection(null, "m1"); + state = ProjectChatUiState.toggleSelection(state, "m2"); + + ProjectChatUiState.ChromeState chromeState = + ProjectChatUiState.resolveChromeState(state, true, "北区试产线回归", "归档确认"); + + assertTrue(chromeState.multiSelecting); + assertFalse(chromeState.showComposer); + assertTrue(chromeState.showMultiSelectBar); + assertFalse(chromeState.showRefresh); + assertFalse(chromeState.showHeaderAction); + assertTrue(chromeState.forwardEnabled); + assertEquals("取消", chromeState.backLabel); + assertEquals("已选 2 条", chromeState.title); + assertEquals("选择要转发的消息", chromeState.subtitle); + } + + @Test + public void chromeStateUsesConversationHeaderWhenNotSelecting() { + ProjectChatUiState.ChromeState chromeState = + ProjectChatUiState.resolveChromeState(ProjectChatUiState.emptySelection(), true, "北区试产线回归", "归档确认"); + + assertFalse(chromeState.multiSelecting); + assertTrue(chromeState.showComposer); + assertFalse(chromeState.showMultiSelectBar); + assertTrue(chromeState.showRefresh); + assertTrue(chromeState.showHeaderAction); + assertFalse(chromeState.forwardEnabled); + assertEquals("返回", chromeState.backLabel); + assertEquals("北区试产线回归", chromeState.title); + assertEquals("归档确认", chromeState.subtitle); + } + + @Test + public void reconcileSelectionDropsMessagesMissingFromRenderSet() { + ProjectChatUiState.SelectionState state = ProjectChatUiState.toggleSelection(null, "m1"); + state = ProjectChatUiState.toggleSelection(state, "m2"); + + ProjectChatUiState.SelectionState reconciled = + ProjectChatUiState.reconcileSelection(state, List.of("m2", "m3")); + + assertTrue(reconciled.multiSelecting); + assertEquals(List.of("m2"), new ArrayList<>(reconciled.selectedMessageIds)); + } + + @Test + public void summarizeForwardBundleTruncatesLongLastMessage() { + String summary = ProjectChatUiState.summarizeForwardBundle( + "这是一条很长很长很长的转发消息摘要,用来验证截断逻辑是否生效并避免卡片过高", + 3 + ); + + assertTrue(summary.startsWith("3 条消息 · 最后一条:")); + assertTrue(summary.endsWith("…")); + assertTrue(summary.contains("这是一条很长很长很长的转发消息摘要")); + } }