fix: stabilize conversation refresh and group create
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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", "北区试产线回归")
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user