chore: polish native wechat ui release v2.5.1

This commit is contained in:
kris
2026-03-29 20:00:00 +08:00
parent fe186ad8d5
commit 062b46bd41
17 changed files with 314 additions and 67 deletions

View File

@@ -36,8 +36,8 @@ android {
applicationId "com.hyzq.boss"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 13
versionName "2.5.0"
versionCode 14
versionName "2.5.1"
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -84,27 +84,15 @@ public class GroupCreateActivity extends BossScreenActivity {
sourceProjectName = threadMeta == null
? sourceProjectName
: threadMeta.optString("threadDisplayName", sourceProjectName == null ? "当前会话" : sourceProjectName);
appendContent(BossUi.buildCard(
this,
"新建独立群聊",
"群聊不是升级原会话,而是以当前会话为源,新建一个独立线程。",
buildSourceMeta(threadMeta, participants)
));
appendContent(BossUi.buildCard(
this,
sourceProjectName,
buildSourceBody(threadMeta, participants),
sourceProjectId + (sourceFolderName.isEmpty() ? "" : " · " + sourceFolderName)
));
} else {
appendContent(BossUi.buildCard(
this,
"从会话首页发起群聊",
"你可以直接把任意线程拉进一个新的独立群聊,原来的单线程会话会保留不变。",
"至少选择 2 个线程"
));
}
for (SummaryCardSpec cardSpec : buildHeaderCardSpecs(
hasSourceProject(),
sourceProjectId,
sourceProjectName,
threadMeta,
participants
)) {
appendContent(BossUi.buildCard(this, cardSpec.title, cardSpec.body, cardSpec.meta));
}
if (rebuildCandidates) {
@@ -133,15 +121,16 @@ public class GroupCreateActivity extends BossScreenActivity {
lastCandidateProjectIds.addAll(nextCandidateProjectIds);
}
SummaryCardSpec selectionCard = buildSelectionCardSpec(
candidates.size(),
selectedProjectIds.size(),
hasSourceProject()
);
appendContent(BossUi.buildCard(
this,
"选择其他线程",
candidates.isEmpty()
? "当前没有可加入的其他线程。"
: selectedProjectIds.isEmpty()
? "你已取消全部勾选,可继续手动选择。"
: "已保留你当前的勾选状态。",
"已选 " + selectedProjectIds.size() + " 个线程"
selectionCard.title,
selectionCard.body,
selectionCard.meta
));
candidateListLayout = new LinearLayout(this);
@@ -203,6 +192,59 @@ public class GroupCreateActivity extends BossScreenActivity {
);
}
static List<SummaryCardSpec> buildHeaderCardSpecs(
boolean hasSourceProject,
@Nullable String sourceProjectId,
@Nullable String sourceProjectName,
@Nullable JSONObject threadMeta,
@Nullable JSONArray participants
) {
List<SummaryCardSpec> cards = new ArrayList<>(1);
if (hasSourceProject) {
String resolvedSourceProjectName = sourceProjectName == null || sourceProjectName.isEmpty()
? "当前会话"
: sourceProjectName;
cards.add(new SummaryCardSpec(
resolvedSourceProjectName,
buildSourceBody(sourceProjectId, threadMeta, participants),
"新群会单独创建,原会话保留"
));
return cards;
}
cards.add(new SummaryCardSpec(
"从会话中发起群聊",
"至少选择 2 个线程,新群会单独创建。",
"原有单线程会话会保留"
));
return cards;
}
static SummaryCardSpec buildSelectionCardSpec(
int candidateCount,
int selectedCount,
boolean hasSourceProject
) {
if (candidateCount <= 0) {
return new SummaryCardSpec(
"选择其他线程",
"当前没有可加入的其他线程",
"候选 0 个 · 已选 0 个"
);
}
if (selectedCount <= 0) {
return new SummaryCardSpec(
"选择其他线程",
hasSourceProject ? "至少选择 1 个其他线程" : "至少选择 2 个线程",
"候选 " + candidateCount + " 个 · 已选 0 个"
);
}
return new SummaryCardSpec(
"选择其他线程",
"继续点选可调整成员",
"候选 " + candidateCount + " 个 · 已选 " + selectedCount + ""
);
}
private LinearLayout buildCandidateRow(CandidateConversation candidate) {
return BossUi.buildConversationRow(
this,
@@ -348,25 +390,33 @@ public class GroupCreateActivity extends BossScreenActivity {
return sourceProjectId != null && !sourceProjectId.isEmpty();
}
private String buildSourceMeta(@Nullable JSONObject threadMeta, @Nullable JSONArray participants) {
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
int count = participants == null ? 0 : participants.length();
String memberLabel = count <= 0 ? "暂无参与线程" : count + " 个参与线程";
if (folderName.isEmpty()) {
return memberLabel;
}
return folderName + " · " + memberLabel;
}
private String buildSourceBody(@Nullable JSONObject threadMeta, @Nullable JSONArray participants) {
private static String buildSourceBody(
@Nullable String sourceProjectId,
@Nullable JSONObject threadMeta,
@Nullable JSONArray participants
) {
String threadId = threadMeta == null ? sourceProjectId : threadMeta.optString("threadId", sourceProjectId);
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
StringBuilder builder = new StringBuilder();
builder.append("来源线程").append(threadId);
builder.append("\n文件夹").append(folderName.isEmpty() ? "未命名文件夹" : folderName);
builder.append("\n参与线程").append(participants == null ? 0 : participants.length());
builder.append("\n默认规则会自动勾选当前会话之外的其他线程");
return builder.toString();
String resolvedFolderName = folderName.isEmpty() ? "未命名文件夹" : folderName;
String resolvedThreadId = threadId == null || threadId.isEmpty() ? "未命名线程" : threadId;
return "来源线程 " + resolvedThreadId
+ " · "
+ resolvedFolderName
+ " · "
+ (participants == null ? 0 : participants.length())
+ " 个参与线程";
}
static final class SummaryCardSpec {
final String title;
final String body;
final String meta;
SummaryCardSpec(String title, String body, String meta) {
this.title = title;
this.body = body;
this.meta = meta;
}
}
private static final class CandidateConversation {

View File

@@ -136,7 +136,7 @@ public final class ProjectChatUiState {
false,
true,
false,
true,
!conversationInfoReady,
conversationInfoReady,
false,
"返回",

View File

@@ -4,6 +4,7 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -12,6 +13,41 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class GroupCreateActivityTest {
@Test
public void buildHeaderCardSpecs_collapsesSourceFlowIntoSingleCompactCard() {
JSONObject threadMeta = new StubJSONObject()
.withString("threadDisplayName", "北区试产线回归")
.withString("threadId", "thread-7")
.withString("folderName", "Mac Studio");
JSONArray participants = new StubJSONArray(
new StubJSONObject(),
new StubJSONObject(),
new StubJSONObject()
);
List<GroupCreateActivity.SummaryCardSpec> cards = GroupCreateActivity.buildHeaderCardSpecs(
true,
"source-1",
"北区试产线回归",
threadMeta,
participants
);
assertEquals(1, cards.size());
assertEquals("北区试产线回归", cards.get(0).title);
assertEquals("来源线程 thread-7 · Mac Studio · 3 个参与线程", cards.get(0).body);
assertEquals("新群会单独创建,原会话保留", cards.get(0).meta);
}
@Test
public void buildSelectionCardSpec_usesCompactWechatStyleHint() {
GroupCreateActivity.SummaryCardSpec card = GroupCreateActivity.buildSelectionCardSpec(5, 0, true);
assertEquals("选择其他线程", card.title);
assertEquals("至少选择 1 个其他线程", card.body);
assertEquals("候选 5 个 · 已选 0 个", card.meta);
}
@Test
public void collectSelectableConversationItems_filtersOutExistingGroupChats() {
JSONObject threadConversation = new StubJSONObject()

View File

@@ -0,0 +1,142 @@
package com.hyzq.boss;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class GroupCreateActivityUiTest {
@Test
public void renderCreatePageUsesCompactSummaryCardsForSourceFlow() throws Exception {
Intent intent = new Intent()
.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_ID, "source-1")
.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_NAME, "北区试产线回归");
TestGroupCreateActivity activity = Robolectric
.buildActivity(TestGroupCreateActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderCreatePage",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildConversationsPayload()),
ReflectionHelpers.ClassParameter.from(boolean.class, true)
);
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "来源线程 thread-7 · Mac Studio · 3 个参与线程"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "新群会单独创建,原会话保留"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "继续点选可调整成员"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "候选 2 个 · 已选 2 个"));
}
@Test
public void toggleSelectionRefreshesRenderedSelectionSummary() throws Exception {
Intent intent = new Intent()
.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_ID, "source-1")
.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_NAME, "北区试产线回归");
TestGroupCreateActivity activity = Robolectric
.buildActivity(TestGroupCreateActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderCreatePage",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildConversationsPayload()),
ReflectionHelpers.ClassParameter.from(boolean.class, true)
);
ReflectionHelpers.callInstanceMethod(
activity,
"toggleSelection",
ReflectionHelpers.ClassParameter.from(String.class, "thread-2")
);
ReflectionHelpers.callInstanceMethod(
activity,
"toggleSelection",
ReflectionHelpers.ClassParameter.from(String.class, "thread-3")
);
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content.getChildAt(1), "至少选择 1 个其他线程"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "候选 2 个 · 已选 0 个"));
}
private static JSONObject buildParticipantsPayload() throws Exception {
JSONObject threadMeta = new JSONObject()
.put("threadDisplayName", "北区试产线回归")
.put("threadId", "thread-7")
.put("folderName", "Mac Studio");
JSONArray participants = new JSONArray()
.put(new JSONObject().put("projectId", "source-1"))
.put(new JSONObject().put("projectId", "thread-2"))
.put(new JSONObject().put("projectId", "thread-3"));
return new JSONObject()
.put("threadMeta", threadMeta)
.put("participants", participants);
}
private static JSONObject buildConversationsPayload() throws Exception {
JSONArray conversations = new JSONArray()
.put(new JSONObject()
.put("projectId", "thread-2")
.put("projectTitle", "硬件审计协作")
.put("folderLabel", "Mac Studio")
.put("lastMessagePreview", "检查摄像头供电链路")
.put("latestReplyLabel", "09:28")
.put("isGroup", false))
.put(new JSONObject()
.put("projectId", "thread-3")
.put("projectTitle", "Boss 移动控制台")
.put("folderLabel", "Boss")
.put("lastMessagePreview", "统一顶部按钮样式")
.put("latestReplyLabel", "09:31")
.put("isGroup", false));
return new JSONObject().put("conversations", conversations);
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
if (expectedText.contentEquals(text)) {
return true;
}
}
if (!(root instanceof ViewGroup)) {
return false;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
return true;
}
}
return false;
}
public static class TestGroupCreateActivity extends GroupCreateActivity {
@Override
protected void reload() {
// Tests drive renderCreatePage manually to avoid network work.
}
}
}

View File

@@ -111,7 +111,7 @@ public class ProjectChatUiStateTest {
assertFalse(chromeState.multiSelecting);
assertTrue(chromeState.showComposer);
assertFalse(chromeState.showMultiSelectBar);
assertTrue(chromeState.showRefresh);
assertFalse(chromeState.showRefresh);
assertTrue(chromeState.showHeaderAction);
assertFalse(chromeState.forwardEnabled);
assertEquals("返回", chromeState.backLabel);
@@ -119,6 +119,22 @@ public class ProjectChatUiStateTest {
assertEquals("归档确认", chromeState.subtitle);
}
@Test
public void chromeStateRestoresRefreshBeforeConversationInfoIsReady() {
ProjectChatUiState.ChromeState chromeState =
ProjectChatUiState.resolveChromeState(ProjectChatUiState.emptySelection(), false, "北区试产线回归", "归档确认");
assertFalse(chromeState.multiSelecting);
assertTrue(chromeState.showComposer);
assertFalse(chromeState.showMultiSelectBar);
assertTrue(chromeState.showRefresh);
assertFalse(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");

View File

@@ -46,7 +46,7 @@ public class ProjectDetailActivityChromeBindingsTest {
assertFalse(bindings.multiSelecting);
assertTrue(bindings.showComposer);
assertFalse(bindings.showMultiSelectBar);
assertTrue(bindings.showRefresh);
assertFalse(bindings.showRefresh);
assertTrue(bindings.showHeaderAction);
assertFalse(bindings.enableForwardButton);
assertTrue(bindings.enablePullRefresh);

View File

@@ -75,7 +75,7 @@ public class ProjectDetailActivityUiTest {
assertEquals(View.VISIBLE, composerRow.getVisibility());
assertEquals(View.GONE, multiSelectActions.getVisibility());
assertEquals("返回", backButton.getText().toString());
assertEquals(View.VISIBLE, refreshButton.getVisibility());
assertEquals(View.GONE, refreshButton.getVisibility());
}
@Test