diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 1ae1c47f..fc4f4ccf 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -100,11 +100,14 @@ fun ChatScreen(viewModel: ChatViewModel) { var isScrolledUp by remember { mutableStateOf(false) } LaunchedEffect(selectedPrivatePeer) { - messageText = TextFieldValue( - selectedPrivatePeer - ?.let(viewModel::conversationDraft) - .orEmpty() - ) + val draft = selectedPrivatePeer + ?.let(viewModel::conversationDraft) + .orEmpty() + messageText = TextFieldValue(draft) + // Setting the field in code does not run onMessageTextChange, so the + // popups have to be re-synced with the restored draft here. + viewModel.updateCommandSuggestions(draft) + viewModel.updateMentionSuggestions(draft) } // Show password dialog when needed @@ -342,6 +345,11 @@ fun ChatScreen(viewModel: ChatViewModel) { text = newText, selection = TextRange(newText.length) ) + // Setting the field in code does not run onMessageTextChange, so + // the popups have to be re-synced with the new text here. The + // inserted mention ends with a space, which hides an open popup. + viewModel.updateCommandSuggestions(newText) + viewModel.updateMentionSuggestions(newText) }, onMessageLongPress = { message -> // Message long press - open user action sheet with message context diff --git a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt index 36c68429..16ca3f16 100644 --- a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt @@ -188,4 +188,48 @@ class CommandProcessorTest() { assertFalse(chatState.getShowMentionSuggestionsValue()) assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) } + + @Test + fun `text set to an inserted mention hides both popups`() { + // A command popup is open while the user taps a nickname in the message list. + commandProcessor.updateCommandSuggestions("/") + assertTrue(chatState.getShowCommandSuggestionsValue()) + + // The composer re-syncs both popups with the inserted text, which ends in a + // space, so neither popup matches it. + commandProcessor.updateCommandSuggestions("/ @alice ") + commandProcessor.updateMentionSuggestions("/ @alice ", meshService, viewModel = null) + assertFalse(chatState.getShowCommandSuggestionsValue()) + assertTrue(chatState.getCommandSuggestionsValue().isEmpty()) + assertFalse(chatState.getShowMentionSuggestionsValue()) + assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) + } + + @Test + fun `a mention followed by a space no longer matches the mention popup`() { + whenever(meshService.myPeerID).thenReturn("me") + whenever(meshService.getPeerNicknames()).thenReturn(mapOf("peer-1" to "alice")) + + commandProcessor.updateMentionSuggestions("@a", meshService, viewModel = null) + assertTrue(chatState.getShowMentionSuggestionsValue()) + + commandProcessor.updateMentionSuggestions("@alice ", meshService, viewModel = null) + assertFalse(chatState.getShowMentionSuggestionsValue()) + assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) + } + + @Test + fun `a restored command draft reopens the command popup`() { + // Switching conversations restores the draft in code; re-syncing with the + // restored text brings the popup back for a command draft. + commandProcessor.updateCommandSuggestions("/j") + assertTrue(chatState.getShowCommandSuggestionsValue()) + assertTrue(chatState.getCommandSuggestionsValue().isNotEmpty()) + + // An empty restored draft leaves both popups hidden. + commandProcessor.updateCommandSuggestions("") + commandProcessor.updateMentionSuggestions("", meshService, viewModel = null) + assertFalse(chatState.getShowCommandSuggestionsValue()) + assertFalse(chatState.getShowMentionSuggestionsValue()) + } }