diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt index 5e5c5b79..52dd188f 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt @@ -10,13 +10,11 @@ import androidx.core.content.ContextCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.m57.hermescontrol.data.model.Attachment -import com.m57.hermescontrol.data.model.BusySendMode import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.ui.common.ActionProgressController import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.verify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -128,37 +126,4 @@ class ChatScreenTest { composeTestRule.onNodeWithTag("chat_input").assertIsDisplayed() composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() } - - @Test - fun chatScreen_primarySendUsesSavedBusyDefault() { - val uiState = - ChatUiState( - connectionStatus = ConnectionStatus.CONNECTED, - isSessionReady = true, - busySendMode = BusySendMode.QUEUE, - ) - val mockViewModel = mockk(relaxed = true) - every { mockViewModel.uiState } returns MutableStateFlow(uiState).asStateFlow() - every { mockViewModel.streamingState } returns MutableStateFlow(StreamingState()).asStateFlow() - every { mockViewModel.timelineState } returns MutableStateFlow(ChatTimelineState()).asStateFlow() - every { mockViewModel.connectionOperationState } returns - MutableStateFlow(ConnectionOperationUiState()).asStateFlow() - every { mockViewModel.actionProgress } returns - ActionProgressController(scope = CoroutineScope(Dispatchers.Main)) - every { mockViewModel.sendMessage(any(), any()) } returns true - - composeTestRule.setContent { - ChatScreen( - onOpenDrawer = {}, - sessionId = null, - viewModel = mockViewModel, - ) - } - - composeTestRule.onNodeWithTag("chat_input").performTextInput("Use my saved default") - composeTestRule.onNodeWithTag("send_button").performClick() - - verify(exactly = 1) { mockViewModel.sendMessage("Use my saved default", null) } - verify(exactly = 0) { mockViewModel.sendMessage("Use my saved default", BusySendMode.CORRECT) } - } } diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt index 63317410..cdda65ee 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt @@ -32,7 +32,6 @@ import androidx.test.filters.MediumTest import androidx.test.platform.app.InstrumentationRegistry import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.Attachment -import com.m57.hermescontrol.data.model.BusySendMode import com.m57.hermescontrol.data.ws.CommandCatalog import org.junit.Assert.assertEquals import org.junit.Rule @@ -58,158 +57,6 @@ class ComposerInteractionTest { private var photosTaps = 0 private var fileTaps = 0 - private fun setContextualComposer( - text: String, - isMainTurnBusy: Boolean, - attachments: List = emptyList(), - onSend: () -> Unit = {}, - onBusySend: (BusySendMode) -> Unit = {}, - isConnected: Boolean = true, - pendingReasoningLevel: String? = null, - ) { - composeTestRule.setContent { - ChatInputBar( - inputFieldValue = TextFieldValue(text), - onInputChange = {}, - onSend = onSend, - onBusySend = onBusySend, - onMicTap = {}, - isListening = false, - isAgentTyping = isMainTurnBusy, - isMainTurnBusy = isMainTurnBusy, - isConnected = isConnected, - commandCatalog = CommandCatalog(), - isSessionReady = true, - pendingAttachments = attachments, - pendingReasoningLevel = pendingReasoningLevel, - ) - } - } - - @Test - fun idleDraft_showsRegularSendWithoutBusyActions() { - setContextualComposer(text = "hello", isMainTurnBusy = false) - - composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() - composeTestRule.onNodeWithTag("queue_button").assertDoesNotExist() - } - - @Test - fun busyBlankDraft_hidesBusyActions() { - setContextualComposer(text = "", isMainTurnBusy = true) - - composeTestRule.onNodeWithTag("send_button").assertDoesNotExist() - composeTestRule.onNodeWithTag("queue_button").assertDoesNotExist() - } - - @Test - fun busyTextDraft_showsSendAndQueueAndRoutesCallbacks() { - var regularSends = 0 - val selected = mutableListOf() - setContextualComposer( - text = "change course", - isMainTurnBusy = true, - onSend = { regularSends++ }, - onBusySend = selected::add, - ) - - composeTestRule.onNodeWithTag("send_button").assertIsDisplayed().performClick() - composeTestRule.onNodeWithTag("queue_button").assertIsDisplayed().performClick() - - composeTestRule.runOnIdle { - assertEquals(1, regularSends) - assertEquals(listOf(BusySendMode.QUEUE), selected) - } - } - - @Test - fun busyAttachmentOnly_showsSendAndQueue() { - val attachment = Attachment("content://document/1", "notes.txt", "text/plain") - setContextualComposer(text = "", isMainTurnBusy = true, attachments = listOf(attachment)) - - composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() - composeTestRule.onNodeWithTag("queue_button").assertIsDisplayed() - } - - @Test - fun busyDisconnectedDraft_disablesSendAndQueue() { - setContextualComposer(text = "hello", isMainTurnBusy = true, isConnected = false) - composeTestRule.onNodeWithTag("send_button").assertIsNotEnabled() - composeTestRule.onNodeWithTag("queue_button").assertIsNotEnabled() - } - - @Test - fun busyPendingReasoning_disablesSendAndQueue() { - setContextualComposer(text = "hello", isMainTurnBusy = true, pendingReasoningLevel = "high") - composeTestRule.onNodeWithTag("send_button").assertIsNotEnabled() - composeTestRule.onNodeWithTag("queue_button").assertIsNotEnabled() - } - - @Test - fun busySlashDraft_usesRegularSendWithoutBusyActions() { - var regularSends = 0 - setContextualComposer( - text = "/stop", - isMainTurnBusy = true, - onSend = { regularSends++ }, - ) - - composeTestRule.onNodeWithTag("queue_button").assertDoesNotExist() - composeTestRule.onNodeWithTag("send_button").performClick() - composeTestRule.runOnIdle { assertEquals(1, regularSends) } - } - - @Test - fun busyBecomesIdle_hidesQueueAndPreservesTypedDraft() { - val busy = mutableStateOf(true) - composeTestRule.setContent { - var input by remember { mutableStateOf(TextFieldValue("draft in progress")) } - ChatInputBar( - inputFieldValue = input, - onInputChange = { input = it }, - onSend = {}, - onBusySend = {}, - onMicTap = {}, - isListening = false, - isAgentTyping = busy.value, - isMainTurnBusy = busy.value, - isConnected = true, - commandCatalog = CommandCatalog(), - isSessionReady = true, - ) - } - - composeTestRule.onNodeWithTag("queue_button").assertIsDisplayed() - composeTestRule.runOnUiThread { busy.value = false } - composeTestRule.onNodeWithTag("queue_button").assertDoesNotExist() - composeTestRule.onNodeWithTag("chat_input").assertTextEquals("draft in progress") - composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() - } - - @Test - fun busyDraft_narrowWidthKeepsSendAndQueueAccessible() { - composeTestRule.setContent { - Box(Modifier.width(220.dp)) { - ChatInputBar( - inputFieldValue = TextFieldValue("change course"), - onInputChange = {}, - onSend = {}, - onBusySend = {}, - onMicTap = {}, - isListening = false, - isAgentTyping = true, - isMainTurnBusy = true, - isConnected = true, - commandCatalog = CommandCatalog(), - isSessionReady = true, - ) - } - } - - composeTestRule.onNodeWithTag("queue_button").assertIsDisplayed() - composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() - } - /** Renders the real input bar with live text and a mic that toggles like ChatMediaLaunchers. */ private fun setComposer( reasoningLevel: String? = "medium", diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/PendingSendPanelTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/PendingSendPanelTest.kt deleted file mode 100644 index ab69dd29..00000000 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/PendingSendPanelTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.m57.hermescontrol.ui.chat.components - -import androidx.activity.ComponentActivity -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.m57.hermescontrol.R -import com.m57.hermescontrol.data.model.BusySendMode -import com.m57.hermescontrol.ui.chat.PendingSend -import com.m57.hermescontrol.ui.chat.PendingSendState -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PendingSendPanelTest { - @get:Rule - val compose = createAndroidComposeRule() - - private fun row(state: PendingSendState) = - PendingSend("receipt", "scope", "session", "My message", mode = BusySendMode.CORRECT, state = state) - - @Test - fun ordinarySendNeverShowsDeliveryPanel() { - val state = mutableStateOf(PendingSendState.SENDING) - compose.setContent { PendingSendPanel(listOf(row(state.value)), mainTurnBusy = false, onSendNow = {}) } - compose.onNodeWithText("My message").assertDoesNotExist() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_sends)).assertDoesNotExist() - compose.runOnIdle { state.value = PendingSendState.ACCEPTED } - compose.onNodeWithText("My message").assertDoesNotExist() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_accepted)).assertDoesNotExist() - } - - @Test - fun queueDisappearsWhenDispatchedWithoutAwaitingLabel() { - val state = mutableStateOf(PendingSendState.QUEUED) - compose.setContent { PendingSendPanel(listOf(row(state.value)), mainTurnBusy = false, onSendNow = {}) } - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_queued)).assertIsDisplayed() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_send_now_warning)).assertDoesNotExist() - compose.runOnIdle { state.value = PendingSendState.SENDING } - compose.onNodeWithText("My message").assertDoesNotExist() - compose.runOnIdle { state.value = PendingSendState.ACCEPTED } - compose.onNodeWithText("My message").assertDoesNotExist() - } - - @Test - fun failedAndUncertainDeliveryRemainRecoverable() { - val state = mutableStateOf(PendingSendState.REJECTED) - var retried: String? = null - compose.setContent { - PendingSendPanel( - listOf(row(state.value)), - mainTurnBusy = false, - onSendNow = { retried = it }, - ) - } - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_rejected)).assertIsDisplayed() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_send_now)).performClick() - compose.runOnIdle { - assertEquals("receipt", retried) - state.value = PendingSendState.UNKNOWN - } - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_unknown)).assertIsDisplayed() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_send_now_warning)).assertIsDisplayed() - } - - @Test - fun busyPanelWarnsAndLabelsEveryRecoverableActionAsStopAndSend() { - val state = mutableStateOf(PendingSendState.QUEUED) - compose.setContent { - PendingSendPanel( - sends = listOf(row(state.value)), - mainTurnBusy = true, - onSendNow = {}, - ) - } - - listOf( - PendingSendState.QUEUED, - PendingSendState.PARKED, - PendingSendState.REJECTED, - PendingSendState.UNKNOWN, - ).forEach { pendingState -> - compose.runOnIdle { state.value = pendingState } - compose - .onNodeWithText( - compose.activity.getString(R.string.chat_pending_send_now_warning), - ).assertIsDisplayed() - compose.onNodeWithText(compose.activity.getString(R.string.chat_busy_stop_and_send)).assertIsDisplayed() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_send_now)).assertDoesNotExist() - compose.onNodeWithText(compose.activity.getString(R.string.chat_pending_send_again)).assertDoesNotExist() - } - } -} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatQueuePresentation.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatQueuePresentation.kt deleted file mode 100644 index c95b505d..00000000 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatQueuePresentation.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.m57.hermescontrol.ui.chat - -/** Presentation only: retain queued messages internally for persistence and receipt matching. */ -internal fun messagesWithoutUnsentQueue( - messages: List, - pendingSends: List, -): List { - val waitingIds = - pendingSends - .filter { it.state == PendingSendState.QUEUED || it.state == PendingSendState.PARKED } - .map { it.id } - .toSet() - if (waitingIds.isEmpty()) return messages - return messages.filterNot { - it.role == MessageRole.USER && it.id in waitingIds && it.canonicalRestId == null - } -} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index a3e8dada..1568e8dd 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -87,7 +87,6 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.model.Attachment import com.m57.hermescontrol.data.model.AttachmentSource -import com.m57.hermescontrol.data.model.BusySendMode import com.m57.hermescontrol.data.model.reasoningSupport import com.m57.hermescontrol.data.session.ActiveSessionHolder import com.m57.hermescontrol.data.ws.ConnectionStatus @@ -178,11 +177,7 @@ fun ChatScreen( // Snapshot-backed search state — read directly so only the scopes that // read its fields recompose on search changes (bar, matched bubbles). val searchState = viewModel.searchState - val sourceMessages = timelineState.historyMessages ?: state.messages - val displayedMessages = - remember(sourceMessages, state.pendingSends) { - messagesWithoutUnsentQueue(sourceMessages, state.pendingSends) - } + val displayedMessages = timelineState.historyMessages ?: state.messages val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current var browserAuthInFlight by rememberSaveable { mutableStateOf(false) } @@ -904,12 +899,6 @@ fun ChatScreen( }, ) - com.m57.hermescontrol.ui.chat.components.PendingSendPanel( - sends = state.pendingSends, - mainTurnBusy = state.isMainTurnBusy, - onSendNow = viewModel::sendQueuedNow, - ) - ChatInputBar( inputFieldValue = inputFieldValue, onInputChange = { inputFieldValue = it }, @@ -920,12 +909,6 @@ fun ChatScreen( scrollController.jumpToBottom(animated = true) } }, - onBusySend = { mode -> - if (viewModel.sendMessage(inputFieldValue.text, mode)) { - inputFieldValue = TextFieldValue("") - scrollController.jumpToBottom(animated = true) - } - }, onMicTap = mediaLaunchers.onMicTap, onMicHoldStart = mediaLaunchers.onMicHoldStart, onMicHoldEnd = mediaLaunchers.onMicHoldEnd, @@ -936,7 +919,6 @@ fun ChatScreen( isVoiceNoteLocked = mediaLaunchers.isVoiceNoteLocked, onStopGeneration = { viewModel.interruptSession() }, isAgentTyping = state.isAgentTyping, - isMainTurnBusy = state.isMainTurnBusy, canInterrupt = state.canInterrupt, isConnected = state.isConnected, isSessionReady = state.isSessionReady && !timelineState.isHistorical, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt index 79638974..2a49d067 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt @@ -73,7 +73,6 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.Attachment -import com.m57.hermescontrol.data.model.BusySendMode import com.m57.hermescontrol.data.model.ProfileInfo import com.m57.hermescontrol.data.ws.CommandBlocklist import com.m57.hermescontrol.data.ws.CommandCatalog @@ -90,11 +89,9 @@ fun ChatInputBar( inputFieldValue: TextFieldValue, onInputChange: (TextFieldValue) -> Unit, onSend: () -> Unit, - onBusySend: (BusySendMode) -> Unit = {}, onMicTap: () -> Unit, isListening: Boolean, isAgentTyping: Boolean, - isMainTurnBusy: Boolean = false, canInterrupt: Boolean = false, isConnected: Boolean, commandCatalog: CommandCatalog, @@ -137,8 +134,6 @@ fun ChatInputBar( pendingReasoningLevel == null && ChatInputPolicy.canSend(inputFieldValue.text, pendingAttachments, isConnected, isSessionReady) val hasDraft = inputFieldValue.text.isNotBlank() || pendingAttachments.isNotEmpty() - val isSlashDraft = inputFieldValue.text.trimStart().startsWith("/") - val showBusyActions = isMainTurnBusy && hasDraft && !isSlashDraft // Attachment tray state var showAttachmentTray by remember { mutableStateOf(false) } @@ -429,10 +424,7 @@ fun ChatInputBar( isListening = isListening, canSend = canSend, showSend = hasDraft, - showQueue = showBusyActions, onSend = onSend, - onQueue = { onBusySend(BusySendMode.QUEUE) }, - onStopAndSend = { onBusySend(BusySendMode.INTERRUPT) }, canInterrupt = canInterrupt, onStopGeneration = onStopGeneration, onAttachTap = { showAttachmentTray = !showAttachmentTray }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt index a85c3699..6b47a49c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt @@ -31,7 +31,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.AddToQueue import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Stop @@ -69,9 +68,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -113,10 +110,7 @@ fun ComposerToolbar( modifier: Modifier = Modifier, canSend: Boolean = false, showSend: Boolean = canSend, - showQueue: Boolean = false, onSend: () -> Unit = {}, - onQueue: () -> Unit = {}, - onStopAndSend: () -> Unit = {}, canDisableReasoning: Boolean? = null, supportsReasoning: Boolean? = null, fastMode: Boolean = false, @@ -562,27 +556,7 @@ fun ComposerToolbar( } } - AnimatedVisibility( - visible = showQueue, - enter = fadeIn() + scaleIn(initialScale = 0.8f), - exit = fadeOut() + scaleOut(targetScale = 0.8f), - ) { - FilledIconButton( - onClick = onQueue, - enabled = canSend, - colors = flatIconButtonColors(palette), - modifier = Modifier.size(48.dp).testTag("queue_button"), - ) { - Icon( - imageVector = Icons.Default.AddToQueue, - contentDescription = stringResource(R.string.chat_busy_queue_action), - modifier = Modifier.size(20.dp), - ) - } - } - // Action button — send when a send is possible, mic / stop otherwise - val stopAndSendLabel = stringResource(R.string.chat_busy_stop_and_send) // While a recording is locked this button finishes the voice note, so // it must stay enabled even when the draft alone would not send // (review, PR #1280). @@ -605,17 +579,7 @@ fun ComposerToolbar( } else { palette.action }, - ).semantics { - if (showSend && showQueue && canSend && !isVoiceNoteLocked) { - customActions = - listOf( - CustomAccessibilityAction(stopAndSendLabel) { - onStopAndSend() - true - }, - ) - } - }.combinedClickable( + ).combinedClickable( enabled = actionEnabled, onClick = { when { @@ -631,8 +595,6 @@ fun ComposerToolbar( else -> onMicTap() } }, - onLongClick = if (showSend && showQueue && !isVoiceNoteLocked) onStopAndSend else null, - onLongClickLabel = if (showSend && showQueue && !isVoiceNoteLocked) stopAndSendLabel else null, ).testTag( when { isVoiceNoteLocked -> "voice_note_send_button" diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/PendingSendPanel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/PendingSendPanel.kt deleted file mode 100644 index 9147e2fe..00000000 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/PendingSendPanel.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.m57.hermescontrol.ui.chat.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.m57.hermescontrol.R -import com.m57.hermescontrol.ui.chat.PendingSend -import com.m57.hermescontrol.ui.chat.PendingSendState - -@Composable -fun PendingSendPanel( - sends: List, - mainTurnBusy: Boolean, - onSendNow: (String) -> Unit, -) { - // Delivery receipts stay internal; only queued work and recovery need UI. - val visibleSends = sends.filter { it.state != PendingSendState.SENDING && it.state != PendingSendState.ACCEPTED } - if (visibleSends.isEmpty()) return - Surface( - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surfaceContainer, - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text(stringResource(R.string.chat_pending_sends), style = MaterialTheme.typography.titleSmall) - if (mainTurnBusy || visibleSends.any { it.state == PendingSendState.UNKNOWN }) { - Text( - stringResource(R.string.chat_pending_send_now_warning), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - LazyColumn(modifier = Modifier.heightIn(max = 170.dp)) { - items(visibleSends, key = { it.id }) { send -> - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Column(modifier = Modifier.weight(1f)) { - Text( - send.text.ifBlank { stringResource(R.string.chat_pending_attachments) }, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = - when (send.state) { - PendingSendState.QUEUED -> stringResource(R.string.chat_pending_queued) - PendingSendState.PARKED -> stringResource(R.string.chat_pending_parked) - PendingSendState.SENDING -> stringResource(R.string.chat_pending_sending) - PendingSendState.ACCEPTED -> stringResource(R.string.chat_pending_accepted) - PendingSendState.UNKNOWN -> stringResource(R.string.chat_pending_unknown) - PendingSendState.REJECTED -> stringResource(R.string.chat_pending_rejected) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - if (send.state in - setOf( - PendingSendState.QUEUED, - PendingSendState.PARKED, - PendingSendState.UNKNOWN, - PendingSendState.REJECTED, - PendingSendState.ACCEPTED, - ) - ) { - TextButton(onClick = { onSendNow(send.id) }) { - Text( - stringResource( - if (mainTurnBusy) { - R.string.chat_busy_stop_and_send - } else if (send.state in - setOf( - PendingSendState.UNKNOWN, - PendingSendState.ACCEPTED, - ) - ) { - R.string.chat_pending_send_again - } else { - R.string.chat_pending_send_now - }, - ), - ) - } - } - } - } - } - } - } -} diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatQueuePresentationTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatQueuePresentationTest.kt deleted file mode 100644 index e11e1dc4..00000000 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatQueuePresentationTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.m57.hermescontrol.ui.chat - -import com.m57.hermescontrol.data.model.BusySendMode -import org.junit.Assert.assertEquals -import org.junit.Assert.assertSame -import org.junit.Assert.assertTrue -import org.junit.Test - -class ChatQueuePresentationTest { - private val message = ChatMessage("queued-id", MessageRole.USER, "Later") - - private fun pending(state: PendingSendState) = - PendingSend(message.id, "scope", "session", message.content, mode = BusySendMode.QUEUE, state = state) - - @Test - fun queuedAndParkedMessagesRemainInternalUntilDispatch() { - val messages = listOf(message) - for (state in listOf(PendingSendState.QUEUED, PendingSendState.PARKED)) { - val receipts = listOf(pending(state)) - assertTrue(messagesWithoutUnsentQueue(messages, receipts).isEmpty()) - assertEquals(listOf(message), messages) - assertEquals(state, receipts.single().state) - } - } - - @Test - fun queuedMessageAppearsOnceWhenSendingStarts() { - val messages = listOf(message) - assertTrue(messagesWithoutUnsentQueue(messages, listOf(pending(PendingSendState.QUEUED))).isEmpty()) - for (state in listOf(PendingSendState.SENDING, PendingSendState.ACCEPTED)) { - assertEquals(messages, messagesWithoutUnsentQueue(messages, listOf(pending(state)))) - } - } - - @Test - fun ordinarySendAndOtherMessagesAreUnchanged() { - val other = ChatMessage("other", MessageRole.USER, "Send now") - val messages = listOf(other, message) - assertSame(messages, messagesWithoutUnsentQueue(messages, emptyList())) - assertEquals(listOf(other), messagesWithoutUnsentQueue(messages, listOf(pending(PendingSendState.QUEUED)))) - } - - @Test - fun canonicalHistoryIsNeverHiddenByStaleQueueReceipt() { - val messages = listOf(message.copy(restId = "rest-session-12")) - assertEquals(messages, messagesWithoutUnsentQueue(messages, listOf(pending(PendingSendState.QUEUED)))) - } -}