Skip to content

Commit c079cca

Browse files
committed
Support non-destructive command mode (DS-030).
Composer state now behaves as a two-level stack: entering a command pushes a clean state on top (stashing pre-command text, picker attachments, and mentions), and cancelling the command pops back to the restored state. Sending a command triggers a full reset — the stash is discarded with everything else. Pure command triggers (e.g. "/" or "/gi") are stripped from the stash so cancelling does not restore phantom trigger characters. Also reorders the attachment picker's onCommandSelected to call selectCommand before consumePickerSession — otherwise the picker clears the composer attachments before the controller can stash them.
1 parent 2d1993d commit c079cca

3 files changed

Lines changed: 329 additions & 4 deletions

File tree

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,11 @@ public data class AttachmentPickerActions(
115115
},
116116
onCreatePollDismissed = {},
117117
onCommandSelected = { command ->
118-
consumePickerSession(attachmentsPickerViewModel, composerViewModel)
118+
// Must run before consumePickerSession: selectCommand stashes the current composer
119+
// attachments for restore-on-cancel, and consumePickerSession would otherwise clear
120+
// them first.
119121
composerViewModel.selectCommand(command)
122+
consumePickerSession(attachmentsPickerViewModel, composerViewModel)
120123
},
121124
onDismiss = { attachmentsPickerViewModel.setPickerVisible(visible = false) },
122125
)

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,26 @@ public class MessageComposerController(
349349
*/
350350
private val selectedMentions: MutableSet<Mention> = mutableSetOf()
351351

352+
/**
353+
* Pre-command input text captured when entering command mode under
354+
* [Config.activeCommandEnabled]. Restored on [clearActiveCommand] when the user dismisses
355+
* the command, and discarded on [clearData] (send / full reset). `null` when no command is
356+
* active or when command mode is disabled.
357+
*/
358+
private var stashedInputValue: String? = null
359+
360+
/**
361+
* Pre-command picker-selected attachments captured when entering command mode. Shares the
362+
* lifecycle of [stashedInputValue]: restored on dismiss, discarded on send.
363+
*/
364+
private var stashedSelectedAttachments: Map<String, Attachment>? = null
365+
366+
/**
367+
* Pre-command mention selections captured when entering command mode. Restored together
368+
* with [stashedInputValue] so mention semantics of the restored draft are preserved.
369+
*/
370+
private var stashedMentions: Set<Mention>? = null
371+
352372
private val mentionSuggester = TypingSuggester(
353373
TypingSuggestionOptions(symbol = MENTION_START_SYMBOL),
354374
)
@@ -705,6 +725,7 @@ public class MessageComposerController(
705725
scope.launch { clearDraftMessage(_state.value.messageMode) }
706726
_messageInput.value = MessageInput()
707727
clearAttachments()
728+
discardCommandStash()
708729
clearActiveCommand()
709730
linkPreviewJob?.cancel()
710731
dismissedLinkPreviewUrl = null
@@ -955,9 +976,18 @@ public class MessageComposerController(
955976
* Sets [MessageComposerState.activeCommand] and clears the text input so the user can type
956977
* the command arguments. The full `/command args` string is assembled in [buildNewMessage].
957978
*
979+
* When [Config.activeCommandEnabled] is `true`, any pre-command input value, picker-selected
980+
* attachments, and mention selections are stashed so [clearActiveCommand] can restore them if
981+
* the user dismisses the command. Re-selecting a command while one is already active does not
982+
* overwrite an existing stash (in-command input is command-specific and not preserved across
983+
* command switches).
984+
*
958985
* @param command The command that was selected.
959986
*/
960987
public fun selectCommand(command: Command) {
988+
if (config.activeCommandEnabled && stashedInputValue == null) {
989+
stashPreCommandState()
990+
}
961991
_state.update { it.copy(activeCommand = command) }
962992
setMessageInputInternal(
963993
value = if (config.activeCommandEnabled) "" else "/${command.name} ",
@@ -967,11 +997,52 @@ public class MessageComposerController(
967997
}
968998

969999
/**
970-
* Dismisses the active command, clearing [MessageComposerState.activeCommand] and resetting the text input.
1000+
* Dismisses the active command, clearing [MessageComposerState.activeCommand].
1001+
*
1002+
* When a pre-command stash exists (populated by [selectCommand] under
1003+
* [Config.activeCommandEnabled]), the stashed input, attachments, and mentions are restored
1004+
* and any text typed inside command mode is discarded. When no stash exists, the input is
1005+
* reset to empty (legacy behaviour).
9711006
*/
9721007
public fun clearActiveCommand() {
9731008
_state.update { it.copy(activeCommand = null) }
974-
setMessageInputInternal("", MessageInput.Source.Default)
1009+
if (!restorePreCommandStateIfAny()) {
1010+
setMessageInputInternal("", MessageInput.Source.Default)
1011+
}
1012+
}
1013+
1014+
private fun stashPreCommandState() {
1015+
val currentText = _messageInput.value.text
1016+
// A pure command trigger (e.g. "/" or "/gi") is not user draft content — it is the
1017+
// popup trigger being consumed by the command. Stash empty instead so cancelling the
1018+
// command does not restore phantom trigger characters.
1019+
stashedInputValue = if (CommandPattern.matcher(currentText).find()) "" else currentText
1020+
stashedSelectedAttachments = LinkedHashMap(_selectedAttachments.value)
1021+
stashedMentions = selectedMentions.toSet()
1022+
_selectedAttachments.value = linkedMapOf()
1023+
selectedMentions.clear()
1024+
_state.update { it.copy(selectedMentions = emptySet()) }
1025+
syncAttachments()
1026+
}
1027+
1028+
private fun restorePreCommandStateIfAny(): Boolean {
1029+
val stashedInput = stashedInputValue ?: return false
1030+
val stashedAttachments = stashedSelectedAttachments.orEmpty()
1031+
val stashedMentionsSnapshot = stashedMentions.orEmpty()
1032+
discardCommandStash()
1033+
setMessageInputInternal(stashedInput, MessageInput.Source.Default)
1034+
_selectedAttachments.value = LinkedHashMap(stashedAttachments)
1035+
selectedMentions.clear()
1036+
selectedMentions.addAll(stashedMentionsSnapshot)
1037+
_state.update { it.copy(selectedMentions = selectedMentions.toSet()) }
1038+
syncAttachments()
1039+
return true
1040+
}
1041+
1042+
private fun discardCommandStash() {
1043+
stashedInputValue = null
1044+
stashedSelectedAttachments = null
1045+
stashedMentions = null
9751046
}
9761047

9771048
/**

0 commit comments

Comments
 (0)