fix: stabilize native chat selection chrome
This commit is contained in:
@@ -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) + "…";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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("这是一条很长很长很长的转发消息摘要"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user