fix: keep chat scroll usable while sending
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -141,18 +141,17 @@
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<Button
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/project_chat_attach"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:text="+"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
android:contentDescription="发送附件"
|
||||
android:padding="12dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_boss_add"
|
||||
android:tint="@color/boss_text_primary" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/project_chat_input"
|
||||
|
||||
@@ -45,7 +45,8 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(0, activity.reloadCount);
|
||||
assertEquals(1, activity.messageReloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,7 +132,8 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(2, activity.reloadCount);
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(1, activity.messageReloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,6 +161,7 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(0, activity.messageReloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -196,7 +199,8 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(0, activity.reloadCount);
|
||||
assertEquals(1, activity.messageReloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -241,13 +245,15 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(0, activity.loadCallCount);
|
||||
assertEquals(1, activity.messageLoadCallCount);
|
||||
assertEquals(0, activity.renderCount);
|
||||
|
||||
activity.releaseFirstLoad();
|
||||
waitFor(() -> 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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user