Merge e8a9e819a89a066cdebf08f6f11bada7435fcaa8 into c127eb83ab94c069c32d37530d2faecd381cd2a8

This commit is contained in:
heyaim 2026-09-14 12:21:32 -05:00 committed by GitHub
commit 1672691011
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 86 additions and 5 deletions

View File

@ -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,14 @@ 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.
// The draft write is skipped the same way; without it, switching
// conversations restores the field without the tapped-in mention.
viewModel.setConversationDraft(selectedPrivatePeer, newText)
viewModel.updateCommandSuggestions(newText)
viewModel.updateMentionSuggestions(newText)
},
onMessageLongPress = { message ->
// Message long press - open user action sheet with message context
@ -431,6 +442,12 @@ fun ChatScreen(viewModel: ChatViewModel) {
text = commandText,
selection = TextRange(commandText.length)
)
// A code-driven edit skips onMessageTextChange: persist the
// draft and re-sync both popups, same as the other
// programmatic-edit sites.
viewModel.setConversationDraft(selectedPrivatePeer, commandText)
viewModel.updateCommandSuggestions(commandText)
viewModel.updateMentionSuggestions(commandText)
},
onMentionSuggestionClick = { mention: String ->
val mentionText = viewModel.selectMentionSuggestion(mention, messageText.text)
@ -438,6 +455,12 @@ fun ChatScreen(viewModel: ChatViewModel) {
text = mentionText,
selection = TextRange(mentionText.length)
)
// A code-driven edit skips onMessageTextChange: persist the
// draft and re-sync both popups, same as the other
// programmatic-edit sites.
viewModel.setConversationDraft(selectedPrivatePeer, mentionText)
viewModel.updateCommandSuggestions(mentionText)
viewModel.updateMentionSuggestions(mentionText)
},
selectedPrivatePeer = null,
currentChannel = currentChannel,

View File

@ -208,4 +208,62 @@ class CommandProcessorTest {
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 `command suggestions close once the input grows past the command`() {
// The command popup only shows while the whole input is a prefix of a
// command name or alias, and none of them contains a space or an @. An
// input that can match the mention popup has already closed the command
// popup.
commandProcessor.updateCommandSuggestions("/m")
assertTrue(chatState.getShowCommandSuggestionsValue())
commandProcessor.updateCommandSuggestions("/msg @ali")
assertFalse(chatState.getShowCommandSuggestionsValue())
assertTrue(chatState.getCommandSuggestionsValue().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())
}
}