fix: stabilize native chat selection chrome

This commit is contained in:
kris
2026-03-28 08:45:20 +08:00
parent 7109f1d3db
commit d2291af32c
3 changed files with 215 additions and 28 deletions

View File

@@ -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<String> availableMessageIds
) {
if (current == null || current.selectedMessageIds.isEmpty() || availableMessageIds == null || availableMessageIds.isEmpty()) {
return emptySelection();
}
LinkedHashSet<String> available = new LinkedHashSet<>(availableMessageIds);
LinkedHashSet<String> 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) + "";
}
}

View File

@@ -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<String> collectMessageIds(@Nullable JSONArray messages) {
ArrayList<String> 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)) {

View File

@@ -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("这是一条很长很长很长的转发消息摘要"));
}
}