diff --git a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java
index ae4eb9b..c0f2800 100644
--- a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java
+++ b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java
@@ -19,6 +19,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
+import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
@@ -61,7 +62,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private LinearLayout quickActionsLayout;
private LinearLayout composerRow;
private LinearLayout multiSelectActionsLayout;
- private Button composerAttachmentButton;
+ private ImageButton composerAttachmentButton;
private EditText composerInput;
private Button composerSendButton;
private Button multiSelectForwardButton;
@@ -105,6 +106,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private boolean realtimeReloadRequiresFullSnapshot;
private boolean reloadInFlight;
private boolean pendingReload;
+ private boolean pendingReloadMessagesOnly;
private boolean pendingReloadForcedScrollToBottom;
private volatile boolean activityDestroyed;
private final Runnable conversationAutoRefreshRunnable = new Runnable() {
@@ -114,7 +116,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!shouldMaintainConversationAutoRefresh()) {
return;
}
- if (!reloadInFlight && !refreshLayout.isRefreshing() && shouldAutoRefreshConversation()) {
+ if (!reloadInFlight && !isComposerBusy() && shouldAutoRefreshConversation()) {
reload(false);
}
armConversationAutoRefresh();
@@ -280,6 +282,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
BossWindowInsets.applyKeyboardAvoidingInset(composerRow);
BossWindowInsets.applyKeyboardAvoidingInset(multiSelectActionsLayout);
+ bindChatRefreshScrollBridge();
updateProjectHeader(initialProjectName == null ? "项目详情" : initialProjectName, "正在同步项目详情...");
if (composerAttachmentButton != null) {
@@ -352,6 +355,18 @@ public class ProjectDetailActivity extends BossScreenActivity {
reload(false);
}
+ private void bindChatRefreshScrollBridge() {
+ if (refreshLayout == null) {
+ return;
+ }
+ refreshLayout.setOnChildScrollUpCallback((parent, child) -> {
+ if (chatScrollView == null || chatScrollView.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ return chatScrollView.canScrollVertically(-1) || chatScrollView.getScrollY() > 0;
+ });
+ }
+
void handleRealtimeEvent(BossRealtimeEvent event) {
if (event == null || event.eventName.isEmpty() || projectId == null || projectId.isEmpty()) {
return;
@@ -371,7 +386,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (tryApplyRealtimeMessagesPatch(event)) {
return;
}
- runOnUiThread(() -> scheduleRealtimeReload(true));
+ runOnUiThread(() -> scheduleRealtimeReload(!"project.messages.updated".equals(event.eventName)));
}
private boolean tryApplyRealtimeMessagesPatch(BossRealtimeEvent event) {
@@ -562,6 +577,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
return;
}
if (reloadInFlight) {
+ pendingReloadMessagesOnly = pendingReload && pendingReloadMessagesOnly && messagesOnly;
+ if (!pendingReload) {
+ pendingReloadMessagesOnly = messagesOnly;
+ }
pendingReload = true;
pendingReloadForcedScrollToBottom = pendingReloadForcedScrollToBottom || forcedScrollToBottom;
return;
@@ -641,20 +660,16 @@ public class ProjectDetailActivity extends BossScreenActivity {
return;
}
boolean forcedScrollToBottom = pendingReloadForcedScrollToBottom;
+ boolean messagesOnly = pendingReloadMessagesOnly;
pendingReload = false;
+ pendingReloadMessagesOnly = false;
pendingReloadForcedScrollToBottom = false;
- reload(forcedScrollToBottom);
+ reloadSnapshot(forcedScrollToBottom, messagesOnly);
}
@Override
protected void setRefreshing(boolean refreshing) {
super.setRefreshing(refreshing);
- if (composerAttachmentButton != null) {
- composerAttachmentButton.setEnabled(!refreshing && !composerSending);
- }
- if (composerInput != null) {
- composerInput.setEnabled(!refreshing);
- }
updateComposerSendButtonState();
updateSelectionUi();
if (!refreshing) {
@@ -788,7 +803,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
&& shouldMaintainConversationAutoRefresh()
&& !reloadInFlight
&& refreshLayout != null
- && !refreshLayout.isRefreshing()) {
+ && !isComposerBusy()) {
reload();
}
updateConversationAutoRefresh();
@@ -1017,7 +1032,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
private void sendProjectMessage(String kind, String body) {
composerSending = true;
updateComposerSendButtonState();
- setRefreshing(true);
appendPendingOutgoingBubble(body);
scrollChatToBottom();
executor.execute(() -> {
@@ -1220,7 +1234,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
private void uploadAttachment(AttachmentComposerState.PendingAttachment attachment) {
composerSending = true;
updateComposerSendButtonState();
- setRefreshing(true);
appendPendingOutgoingAttachment(attachment);
scrollChatToBottom();
executor.execute(() -> {
@@ -2361,8 +2374,13 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (composerSendButton == null || composerInput == null) {
return;
}
+ boolean composerBusy = isComposerBusy();
+ if (composerAttachmentButton != null) {
+ composerAttachmentButton.setEnabled(!composerBusy);
+ }
+ composerInput.setEnabled(!composerBusy);
String body = composerInput.getText() == null ? "" : composerInput.getText().toString();
- composerSendButton.setEnabled(ProjectChatUiState.canSend(body, isComposerBusy()));
+ composerSendButton.setEnabled(ProjectChatUiState.canSend(body, composerBusy));
}
private boolean isComposerBusy() {
@@ -2698,7 +2716,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
composerSending = true;
updateComposerSendButtonState();
- setRefreshing(true);
showMessage(waitingMessage);
enqueueReplyWaitPoll(waitSpec.baselineMessageId, includeDispatchPlans);
}
@@ -2781,7 +2798,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!hasReply && !isMasterAgentConversation()) {
composerSending = true;
updateComposerSendButtonState();
- setRefreshing(true);
}
});
renderedInitialSnapshot = true;
diff --git a/android/app/src/main/res/layout/activity_project_chat.xml b/android/app/src/main/res/layout/activity_project_chat.xml
index 8613603..fcce8e5 100644
--- a/android/app/src/main/res/layout/activity_project_chat.xml
+++ b/android/app/src/main/res/layout/activity_project_chat.xml
@@ -141,18 +141,17 @@
android:paddingRight="12dp"
android:paddingBottom="12dp">
-
+ android:contentDescription="发送附件"
+ android:padding="12dp"
+ android:scaleType="center"
+ android:src="@drawable/ic_boss_add"
+ android:tint="@color/boss_text_primary" />
activity.renderCount == 2 && activity.loadCallCount == 2);
+ waitFor(() -> activity.renderCount == 2 && activity.messageLoadCallCount == 1 && activity.loadCallCount == 1);
- assertEquals(2, activity.loadCallCount);
+ assertEquals(1, activity.loadCallCount);
+ assertEquals(1, activity.messageLoadCallCount);
assertEquals(2, activity.renderCount);
}
@@ -313,7 +319,9 @@ public class ProjectDetailActivityRealtimeTest {
public static class TestRealtimeProjectDetailActivity extends ProjectDetailActivity {
int reloadCount;
+ int messageReloadCount;
volatile int loadCallCount;
+ volatile int messageLoadCallCount;
volatile int renderCount;
private CountDownLatch firstLoadStarted;
private CountDownLatch releaseFirstLoad;
@@ -344,6 +352,26 @@ public class ProjectDetailActivityRealtimeTest {
super.reload();
}
+ @Override
+ ProjectSnapshot loadProjectMessagesSnapshotForRefresh() throws Exception {
+ messageReloadCount += 1;
+ messageLoadCallCount += 1;
+ if (messageLoadCallCount == 1 && firstLoadStarted != null && releaseFirstLoad != null) {
+ firstLoadStarted.countDown();
+ releaseFirstLoad.await(2, TimeUnit.SECONDS);
+ }
+ return new ProjectSnapshot(
+ new JSONObject().put(
+ "project",
+ new JSONObject()
+ .put("name", "北区试产线")
+ .put("messages", new JSONArray())
+ ),
+ null,
+ null
+ );
+ }
+
@Override
ProjectSnapshot loadProjectSnapshotForRefresh() throws Exception {
loadCallCount += 1;
diff --git a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java
index 6a677bf..4602f95 100644
--- a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java
+++ b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java
@@ -17,6 +17,7 @@ import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.TextView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.appcompat.app.AlertDialog;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -131,6 +132,22 @@ public class ProjectDetailActivityUiTest {
assertEquals(R.id.project_chat_scroll, ((View) contentLayout.getParent()).getId());
}
+ @Test
+ public void chatRefreshUsesScrollViewAsChildScrollReference() {
+ Intent intent = new Intent()
+ .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
+ .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
+ TestProjectDetailActivity activity = Robolectric
+ .buildActivity(TestProjectDetailActivity.class, intent)
+ .setup()
+ .get();
+
+ SwipeRefreshLayout refreshLayout = activity.findViewById(R.id.screen_refresh_layout);
+
+ Object childScrollCallback = ReflectionHelpers.getField(refreshLayout, "mChildScrollUpCallback");
+ assertNotNull(childScrollCallback);
+ }
+
@Test
public void composerRowLayoutChangeWithFocusedInput_scrollsChatToBottomAgain() {
Intent intent = new Intent()
@@ -176,6 +193,48 @@ public class ProjectDetailActivityUiTest {
assertEquals(baselineScrollCount, activity.scrollChatToBottomCount);
}
+ @Test
+ public void composerBusyWithoutRefreshSpinnerStillDisablesComposerInputs() {
+ Intent intent = new Intent()
+ .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
+ .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
+ TestProjectDetailActivity activity = Robolectric
+ .buildActivity(TestProjectDetailActivity.class, intent)
+ .setup()
+ .get();
+
+ ReflectionHelpers.setField(activity, "composerSending", true);
+ ReflectionHelpers.callInstanceMethod(activity, "updateComposerSendButtonState");
+
+ Button sendButton = activity.findViewById(R.id.project_chat_send);
+ View composerInput = activity.findViewById(R.id.project_chat_input);
+ View attachView = activity.findViewById(R.id.project_chat_attach);
+ SwipeRefreshLayout refreshLayout = activity.findViewById(R.id.screen_refresh_layout);
+
+ assertFalse(refreshLayout.isRefreshing());
+ assertFalse(sendButton.isEnabled());
+ assertFalse(composerInput.isEnabled());
+ assertFalse(attachView.isEnabled());
+ }
+
+ @Test
+ public void composerAttachmentUsesIconButtonFrameInsteadOfTextButton() {
+ Intent intent = new Intent()
+ .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
+ .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
+ TestProjectDetailActivity activity = Robolectric
+ .buildActivity(TestProjectDetailActivity.class, intent)
+ .setup()
+ .get();
+
+ View attachView = activity.findViewById(R.id.project_chat_attach);
+
+ assertTrue(attachView instanceof ImageButton);
+ ViewGroup.LayoutParams params = attachView.getLayoutParams();
+ assertTrue(params.width >= BossUi.dp(activity, 46));
+ assertTrue(params.height >= BossUi.dp(activity, 46));
+ }
+
@Test
public void manualAnalysisAttachmentShowsActionChip() throws Exception {
Intent intent = new Intent()