fix: stabilize conversation refresh and group create

This commit is contained in:
kris
2026-04-03 09:43:03 +08:00
parent 354c8b1f0b
commit 95f164e552
6 changed files with 204 additions and 7 deletions

View File

@@ -29,6 +29,7 @@ import java.util.Map;
public class BossApiClient {
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 12000;
private static final int DEFAULT_READ_TIMEOUT_MS = 12000;
private static final int CONVERSATIONS_READ_TIMEOUT_MS = 30000;
private static final int CHAT_FLOW_READ_TIMEOUT_MS = 65000;
private static final int CHAT_SEND_READ_TIMEOUT_MS = 20000;
private static final String PREFS_NAME = "boss_native_client";
@@ -79,7 +80,13 @@ public class BossApiClient {
}
public ApiResponse getConversations() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/conversations", null);
return requestWithRestoreRaw(
"GET",
"/api/v1/conversations",
null,
DEFAULT_CONNECT_TIMEOUT_MS,
CONVERSATIONS_READ_TIMEOUT_MS
);
}
public ApiResponse getConversationHome() throws IOException, JSONException {

View File

@@ -80,8 +80,8 @@ public class GroupCreateActivity extends BossScreenActivity {
cachedConversationsPayload = conversationsPayload;
replaceContent();
JSONObject threadMeta = participantsPayload.optJSONObject("threadMeta");
JSONArray participants = participantsPayload.optJSONArray("participants");
JSONObject threadMeta = participantsPayload == null ? null : participantsPayload.optJSONObject("threadMeta");
JSONArray participants = participantsPayload == null ? null : participantsPayload.optJSONArray("participants");
sourceFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
if (hasSourceProject()) {
sourceProjectName = threadMeta == null

View File

@@ -2,8 +2,11 @@ package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -60,6 +63,7 @@ public class MainActivity extends AppCompatActivity {
private @Nullable JSONArray devicesData;
private @Nullable String boundDeviceId;
private @Nullable String boundDeviceName;
private String conversationSearchQuery = "";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -256,6 +260,17 @@ public class MainActivity extends AppCompatActivity {
} catch (Exception ignored) {
conversationsOk = false;
}
if (!conversationsOk) {
try {
BossApiClient.ApiResponse fallbackConversations = apiClient.getConversationHome();
if (fallbackConversations.ok()) {
conversations = fallbackConversations;
conversationsOk = true;
}
} catch (Exception ignored) {
conversationsOk = false;
}
}
try {
devices = apiClient.getDevices();
devicesOk = devices.ok();
@@ -442,14 +457,15 @@ public class MainActivity extends AppCompatActivity {
private void renderConversationsRoot() {
screenContent.removeAllViews();
screenContent.addView(BossUi.buildHintPill(this, WechatSurfaceMapper.conversationsHintPillText()));
if (conversationsData == null || conversationsData.length() == 0) {
screenContent.addView(buildConversationSearchInput());
JSONArray filteredConversations = filterConversationItems(conversationsData, conversationSearchQuery);
if (filteredConversations == null || filteredConversations.length() == 0) {
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有会话数据。"));
return;
}
for (int i = 0; i < conversationsData.length(); i++) {
JSONObject item = conversationsData.optJSONObject(i);
for (int i = 0; i < filteredConversations.length(); i++) {
JSONObject item = filteredConversations.optJSONObject(i);
if (item == null) continue;
String projectId = item.optString("projectId", "");
String conversationType = item.optString("conversationType", "");
@@ -477,6 +493,81 @@ public class MainActivity extends AppCompatActivity {
}
}
private EditText buildConversationSearchInput() {
EditText input = BossUi.buildInput(this, "搜索项目或线程", false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = BossUi.dp(this, 16);
params.rightMargin = BossUi.dp(this, 16);
params.bottomMargin = BossUi.dp(this, 10);
input.setLayoutParams(params);
input.setText(conversationSearchQuery);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable editable) {
String nextQuery = editable == null ? "" : editable.toString();
if (nextQuery.equals(conversationSearchQuery)) {
return;
}
conversationSearchQuery = nextQuery;
renderConversationsRoot();
}
});
return input;
}
static JSONArray filterConversationItems(@Nullable JSONArray source, @Nullable String rawQuery) {
if (source == null) {
return null;
}
String query = rawQuery == null ? "" : rawQuery.trim().toLowerCase();
if (query.isEmpty()) {
return source;
}
JSONArray filtered = new JSONArray();
for (int i = 0; i < source.length(); i++) {
JSONObject item = source.optJSONObject(i);
if (item == null) {
continue;
}
if (matchesConversationQuery(item, query)) {
filtered.put(item);
}
}
return filtered;
}
static boolean matchesConversationQuery(JSONObject item, String rawQuery) {
if (item == null) {
return false;
}
String query = rawQuery == null ? "" : rawQuery.trim().toLowerCase();
if (query.isEmpty()) {
return true;
}
String[] fields = new String[] {
item.optString("projectTitle", ""),
item.optString("threadTitle", ""),
item.optString("folderLabel", ""),
item.optString("lastMessagePreview", ""),
item.optString("preview", "")
};
for (String field : fields) {
if (field != null && field.toLowerCase().contains(query)) {
return true;
}
}
return false;
}
private void renderDevicesRoot() {
screenContent.removeAllViews();
if (devicesData == null || devicesData.length() == 0) {

View File

@@ -39,6 +39,20 @@ public class BossApiClientDispatchPlansTest {
assertEquals("GET", connection.requestMethodValue);
}
@Test
public void getConversationsUsesExtendedReadTimeoutForFullThreadList() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/conversations"));
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.getConversations();
assertEquals(200, response.statusCode);
assertEquals("/api/v1/conversations", apiClient.lastPath);
assertEquals("GET", connection.requestMethodValue);
assertEquals(12000, connection.connectTimeoutValue);
assertEquals(30000, connection.readTimeoutValue);
}
@Test
public void confirmDispatchPlanWritesApprovedTargetProjectIds() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/p1/dispatch-plans/plan-1/confirm"));

View File

@@ -104,6 +104,28 @@ public class GroupCreateActivityUiTest {
assertTrue(viewTreeContainsText(lastChild, "创建群聊"));
}
@Test
public void renderCreatePageSupportsRootCreateFlowWithoutParticipantsPayload() throws Exception {
Intent intent = new Intent();
TestGroupCreateActivity activity = Robolectric
.buildActivity(TestGroupCreateActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderCreatePage",
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
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), "从会话列表直接建群"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程"));
}
private static JSONObject buildParticipantsPayload() throws Exception {
JSONObject threadMeta = new JSONObject()
.put("threadDisplayName", "北区试产线回归")

View File

@@ -0,0 +1,63 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.widget.EditText;
import android.widget.LinearLayout;
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 MainActivityConversationSearchTest {
@Test
public void filterConversationItemsMatchesProjectTitleAndFolder() throws Exception {
JSONArray source = new JSONArray()
.put(new JSONObject()
.put("projectId", "p1")
.put("projectTitle", "500Gcode")
.put("folderLabel", "Mac Studio")
.put("lastMessagePreview", "线程链路正常"))
.put(new JSONObject()
.put("projectId", "p2")
.put("projectTitle", "Figma 联调")
.put("folderLabel", "设计")
.put("lastMessagePreview", "等待审阅"));
JSONArray filteredByProject = MainActivity.filterConversationItems(source, "500g");
JSONArray filteredByFolder = MainActivity.filterConversationItems(source, "设计");
assertEquals(1, filteredByProject.length());
assertEquals("p1", filteredByProject.optJSONObject(0).optString("projectId", ""));
assertEquals(1, filteredByFolder.length());
assertEquals("p2", filteredByFolder.optJSONObject(0).optString("projectId", ""));
}
@Test
public void renderConversationsRootShowsSearchInputInsteadOfHintPill() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
ReflectionHelpers.setField(activity, "conversationsData", new JSONArray()
.put(new JSONObject()
.put("projectId", "p1")
.put("projectTitle", "500Gcode")
.put("folderLabel", "Mac Studio")
.put("lastMessagePreview", "线程链路正常")
.put("latestReplyLabel", "09:40")));
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(activity, "renderConversationsRoot");
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(content.getChildAt(0) instanceof EditText);
EditText input = (EditText) content.getChildAt(0);
assertEquals("搜索项目或线程", String.valueOf(input.getHint()));
}
}