Skip to content

Commit 79f2abf

Browse files
committed
Replace MessageComposerNotice with MessageComposerViewEvent to handle transient UI feedback as one-shot side effects.
- Rename `MessageComposerNotice` to `MessageComposerViewEvent`. - Replace the `notices` list in `MessageComposerState` with an `events` `SharedFlow` in `MessageComposerController`. - Remove `dismissNotice()` and `emitNotice()` in favor of direct event emission. - Update `MessageComposerController` to emit `CommandUnavailable` events when commands are triggered in incompatible modes (e.g., during editing). - Update unit tests to collect and verify events using the `test` library.
1 parent 7b7a51c commit 79f2abf

6 files changed

Lines changed: 76 additions & 120 deletions

File tree

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import io.getstream.chat.android.ui.common.state.messages.MessageInput
3131
import io.getstream.chat.android.ui.common.state.messages.MessageMode
3232
import io.getstream.chat.android.ui.common.state.messages.Reply
3333
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
34+
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerViewEvent
3435
import io.getstream.chat.android.ui.common.utils.typing.TypingUpdatesBuffer
3536
import io.getstream.result.Result
3637
import io.getstream.result.call.Call
@@ -56,6 +57,12 @@ public class MessageComposerViewModel(
5657
*/
5758
public val inputFocusEvents: SharedFlow<Unit> = messageComposerController.inputFocusEvents
5859

60+
/**
61+
* One-shot events emitted by the composer, such as feedback for unavailable commands. UI
62+
* layers collect this flow and react with transient UI.
63+
*/
64+
public val events: SharedFlow<MessageComposerViewEvent> = messageComposerController.events
65+
5966
/**
6067
* The full UI state that has all the required data.
6168
*/

stream-chat-android-ui-common/api/stream-chat-android-ui-common.api

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2281,21 +2281,6 @@ public final class io/getstream/chat/android/ui/common/state/messages/composer/A
22812281
public fun toString ()Ljava/lang/String;
22822282
}
22832283

2284-
public abstract interface class io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice {
2285-
}
2286-
2287-
public final class io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice$CommandUnavailable : io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice {
2288-
public static final field $stable I
2289-
public fun <init> (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)V
2290-
public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/MessageAction;
2291-
public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice$CommandUnavailable;
2292-
public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice$CommandUnavailable;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice$CommandUnavailable;
2293-
public fun equals (Ljava/lang/Object;)Z
2294-
public final fun getAction ()Lio/getstream/chat/android/ui/common/state/messages/MessageAction;
2295-
public fun hashCode ()I
2296-
public fun toString ()Ljava/lang/String;
2297-
}
2298-
22992284
public final class io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState {
23002285
public static final field $stable I
23012286
public fun <init> ()V
@@ -2317,8 +2302,7 @@ public final class io/getstream/chat/android/ui/common/state/messages/composer/M
23172302
public fun <init> (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZ)V
23182303
public fun <init> (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;)V
23192304
public fun <init> (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;)V
2320-
public fun <init> (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;Ljava/util/List;)V
2321-
public synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
2305+
public synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
23222306
public final fun component1 ()Ljava/lang/String;
23232307
public final fun component10 ()Z
23242308
public final fun component11 ()Ljava/util/Set;
@@ -2329,7 +2313,6 @@ public final class io/getstream/chat/android/ui/common/state/messages/composer/M
23292313
public final fun component16 ()Z
23302314
public final fun component17 ()Ljava/util/Set;
23312315
public final fun component18 ()Lio/getstream/chat/android/models/Command;
2332-
public final fun component19 ()Ljava/util/List;
23332316
public final fun component2 ()Ljava/util/List;
23342317
public final fun component3 ()Lio/getstream/chat/android/ui/common/state/messages/MessageAction;
23352318
public final fun component4 ()Ljava/util/List;
@@ -2338,8 +2321,8 @@ public final class io/getstream/chat/android/ui/common/state/messages/composer/M
23382321
public final fun component7 ()Lio/getstream/chat/android/models/LinkPreview;
23392322
public final fun component8 ()I
23402323
public final fun component9 ()Lio/getstream/chat/android/ui/common/state/messages/MessageMode;
2341-
public final fun copy (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;Ljava/util/List;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;
2342-
public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;
2324+
public final fun copy (Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;
2325+
public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/models/LinkPreview;ILio/getstream/chat/android/ui/common/state/messages/MessageMode;ZLjava/util/Set;ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;ZZLjava/util/Set;Lio/getstream/chat/android/models/Command;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;
23432326
public fun equals (Ljava/lang/Object;)Z
23442327
public final fun getAction ()Lio/getstream/chat/android/ui/common/state/messages/MessageAction;
23452328
public final fun getActiveCommand ()Lio/getstream/chat/android/models/Command;
@@ -2353,7 +2336,6 @@ public final class io/getstream/chat/android/ui/common/state/messages/composer/M
23532336
public final fun getLinkPreview ()Lio/getstream/chat/android/models/LinkPreview;
23542337
public final fun getMentionSuggestions ()Ljava/util/List;
23552338
public final fun getMessageMode ()Lio/getstream/chat/android/ui/common/state/messages/MessageMode;
2356-
public final fun getNotices ()Ljava/util/List;
23572339
public final fun getOwnCapabilities ()Ljava/util/Set;
23582340
public final fun getPollsEnabled ()Z
23592341
public final fun getRecording ()Lio/getstream/chat/android/ui/common/state/messages/composer/RecordingState;
@@ -2364,6 +2346,21 @@ public final class io/getstream/chat/android/ui/common/state/messages/composer/M
23642346
public fun toString ()Ljava/lang/String;
23652347
}
23662348

2349+
public abstract interface class io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent {
2350+
}
2351+
2352+
public final class io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent$CommandUnavailable : io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent {
2353+
public static final field $stable I
2354+
public fun <init> (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)V
2355+
public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/MessageAction;
2356+
public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent$CommandUnavailable;
2357+
public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent$CommandUnavailable;Lio/getstream/chat/android/ui/common/state/messages/MessageAction;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent$CommandUnavailable;
2358+
public fun equals (Ljava/lang/Object;)Z
2359+
public final fun getAction ()Lio/getstream/chat/android/ui/common/state/messages/MessageAction;
2360+
public fun hashCode ()I
2361+
public fun toString ()Ljava/lang/String;
2362+
}
2363+
23672364
public abstract class io/getstream/chat/android/ui/common/state/messages/composer/RecordingState {
23682365
public static final field $stable I
23692366
}

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

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ import io.getstream.chat.android.ui.common.state.messages.MessageInput
4545
import io.getstream.chat.android.ui.common.state.messages.MessageMode
4646
import io.getstream.chat.android.ui.common.state.messages.Reply
4747
import io.getstream.chat.android.ui.common.state.messages.ThreadReply
48-
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerNotice
4948
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
49+
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerViewEvent
5050
import io.getstream.chat.android.ui.common.state.messages.composer.MessageValidator
5151
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState
5252
import io.getstream.chat.android.ui.common.state.messages.composer.isAvailableFor
@@ -254,6 +254,14 @@ public class MessageComposerController(
254254
*/
255255
public val inputFocusEvents: SharedFlow<Unit> = _inputFocusEvents.asSharedFlow()
256256

257+
private val _events = MutableSharedFlow<MessageComposerViewEvent>(extraBufferCapacity = 1)
258+
259+
/**
260+
* One-shot events emitted by the composer, such as feedback for unavailable commands. UI
261+
* layers collect this flow and react with transient UI (e.g. a snackbar).
262+
*/
263+
public val events: SharedFlow<MessageComposerViewEvent> = _events.asSharedFlow()
264+
257265
// Insertion-ordered map keyed by attachment key (EXTRA_SOURCE_URI or fallback).
258266
// Tracks picker selections independently of edit-mode attachments, so selections
259267
// survive entering and exiting edit mode.
@@ -973,15 +981,15 @@ public class MessageComposerController(
973981
* command switches).
974982
*
975983
* When the command is not available for the current composer action (edit mode or a
976-
* moderation command during reply), emits a [MessageComposerNotice.CommandUnavailable]
977-
* into [MessageComposerState.notices] and returns without changing the active command.
984+
* moderation command during reply), emits a [MessageComposerViewEvent.CommandUnavailable]
985+
* on [events] and returns without changing the active command.
978986
*
979987
* @param command The command that was selected.
980988
*/
981989
public fun selectCommand(command: Command) {
982990
val action = activeAction
983991
if (!command.isAvailableFor(action)) {
984-
if (action != null) emitNotice(MessageComposerNotice.CommandUnavailable(action))
992+
if (action != null) _events.tryEmit(MessageComposerViewEvent.CommandUnavailable(action))
985993
return
986994
}
987995
if (config.activeCommandEnabled && commandStash == null) {
@@ -1010,21 +1018,6 @@ public class MessageComposerController(
10101018
}
10111019
}
10121020

1013-
/**
1014-
* Removes [notice] from [MessageComposerState.notices]. Call this from the UI layer after
1015-
* a notice has been rendered and dismissed. Identical notices are removed one at a time
1016-
* so queued snackbars drain in order.
1017-
*
1018-
* @param notice The notice to remove.
1019-
*/
1020-
public fun dismissNotice(notice: MessageComposerNotice) {
1021-
_state.update { it.copy(notices = it.notices - notice) }
1022-
}
1023-
1024-
private fun emitNotice(notice: MessageComposerNotice) {
1025-
_state.update { it.copy(notices = it.notices + notice) }
1026-
}
1027-
10281021
private fun stashPreCommandState() {
10291022
val currentText = _messageInput.value.text
10301023
// A pure command trigger (e.g. "/" or "/gi") is not user draft content — it is the
@@ -1206,15 +1199,15 @@ public class MessageComposerController(
12061199
* Shows the command suggestion list popup if necessary.
12071200
*
12081201
* While the composer is in edit mode, typing a command trigger suppresses the popup and
1209-
* emits a [MessageComposerNotice.CommandUnavailable] so the UI can inform the user that
1202+
* emits a [MessageComposerViewEvent.CommandUnavailable] so the UI can inform the user that
12101203
* commands are blocked.
12111204
*/
12121205
private fun handleCommandSuggestions() {
12131206
val containsCommand = CommandPattern.matcher(messageText).find()
12141207
val action = activeAction
12151208
if (containsCommand && action is Edit) {
12161209
_state.update { it.copy(commandSuggestions = emptyList()) }
1217-
emitNotice(MessageComposerNotice.CommandUnavailable(action))
1210+
_events.tryEmit(MessageComposerViewEvent.CommandUnavailable(action))
12181211
return
12191212
}
12201213
val suggestions = if (containsCommand) {

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ import io.getstream.chat.android.ui.common.state.messages.MessageMode
5151
* editable unless the user doesn't have proper [ChannelCapabilities] to send messages, otherwise it's disabled.
5252
* @param selectedMentions The list of selected mentions in the current input.
5353
* @param activeCommand The command that is currently active (selected from the suggestion popup).
54-
* @param notices Transient feedback messages emitted by the composer (e.g. when the user attempts
55-
* an action that is unavailable for the current [action]). The UI layer renders each notice and
56-
* removes it via
57-
* [io.getstream.chat.android.ui.common.feature.messages.composer.MessageComposerController.dismissNotice].
5854
*/
5955
public data class MessageComposerState @JvmOverloads constructor(
6056
val inputValue: String = "",
@@ -75,5 +71,4 @@ public data class MessageComposerState @JvmOverloads constructor(
7571
val sendEnabled: Boolean = true,
7672
val selectedMentions: Set<Mention> = emptySet(),
7773
val activeCommand: Command? = null,
78-
val notices: List<MessageComposerNotice> = emptyList(),
7974
)

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerNotice.kt renamed to stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent.kt

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,20 @@ package io.getstream.chat.android.ui.common.state.messages.composer
1919
import io.getstream.chat.android.ui.common.state.messages.MessageAction
2020

2121
/**
22-
* Transient feedback surfaced by the composer. Emitted into [MessageComposerState.notices] and
23-
* consumed by the UI layer, which typically renders each notice as a snackbar and removes it
24-
* via [io.getstream.chat.android.ui.common.feature.messages.composer.MessageComposerController.dismissNotice]
25-
* once rendered.
26-
*
27-
* In v8 this hierarchy may absorb [ValidationError] so the composer exposes a single notice stream
28-
* for both blocking validation and informational feedback.
22+
* One-shot side-effect events emitted by the composer. Consumers typically collect these from
23+
* [io.getstream.chat.android.ui.common.feature.messages.composer.MessageComposerController.events]
24+
* and react with transient UI (e.g. a snackbar).
2925
*/
30-
public interface MessageComposerNotice {
26+
public interface MessageComposerViewEvent {
3127

3228
/**
33-
* Emitted when the user attempts to select or trigger a command that is not available for
34-
* the current composer [action] (e.g. tapping a moderation command while in reply mode,
35-
* or typing `/` while in edit mode).
29+
* Emitted when the user attempts to trigger a command that is not available for the current
30+
* composer [action] (e.g. tapping a moderation command while in reply mode, or typing `/`
31+
* while in edit mode).
3632
*
3733
* @param action The composer action that blocked the command.
3834
*/
3935
public data class CommandUnavailable(
4036
val action: MessageAction,
41-
) : MessageComposerNotice
37+
) : MessageComposerViewEvent
4238
}

0 commit comments

Comments
 (0)