Skip to content

Commit ea20778

Browse files
authored
Add composer autocomplete for enhanced mentions in Compose SDK (#6503)
* Add Compose composer autocomplete for enhanced mentions * Address PR feedback * Stub member query in non-user mention tests so user lookup returns empty
1 parent 9acde86 commit ea20778

39 files changed

Lines changed: 1390 additions & 291 deletions

File tree

stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ open class MessageListPage {
6868
val commandsButton get() = By.res("Stream_ComposerCommandsButton")
6969
val commandSuggestionList get() = By.res("Stream_CommandSuggestionList")
7070
val commandSuggestionListTitle get() = By.res("Stream_CommandSuggestionListTitle")
71-
val userSuggestion get() = By.res("Stream_UserSuggestionItem")
71+
val userSuggestion get() = By.res("Stream_SuggestionItem")
7272
val giphyButton get() = By.res("Stream_SuggestionListGiphyButton")
7373
val attachmentsButton get() = By.res("Stream_ComposerAttachmentsButton")
7474
val quotedMessage get() = By.res("Stream_QuotedMessage")

stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class ChatsActivity : ComponentActivity() {
258258
ChatListContentMode.Channels,
259259
ChatListContentMode.Mentions,
260260
-> AppBottomBarOption.CHATS
261+
261262
ChatListContentMode.Threads -> AppBottomBarOption.THREADS
262263
}
263264
AppBottomBar(

stream-chat-android-compose/api/stream-chat-android-compose.api

Lines changed: 86 additions & 8 deletions
Large diffs are not rendered by default.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ import io.getstream.chat.android.compose.R
4949
import io.getstream.chat.android.compose.ui.components.composer.MessageInput
5050
import io.getstream.chat.android.compose.ui.messages.composer.actions.AudioRecordingActions
5151
import io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions.CommandSuggestionList
52+
import io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions.MentionSuggestionList
5253
import io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions.SuggestionsMenu
53-
import io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions.UserSuggestionList
5454
import io.getstream.chat.android.compose.ui.theme.ChatTheme
5555
import io.getstream.chat.android.compose.ui.theme.ComposerConfig
5656
import io.getstream.chat.android.compose.ui.theme.LocalChatUiConfig
@@ -73,6 +73,7 @@ import io.getstream.chat.android.models.Message
7373
import io.getstream.chat.android.models.User
7474
import io.getstream.chat.android.previewdata.PreviewCommandData
7575
import io.getstream.chat.android.previewdata.PreviewUserData
76+
import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention
7677
import io.getstream.chat.android.ui.common.state.messages.Edit
7778
import io.getstream.chat.android.ui.common.state.messages.Reply
7879
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
@@ -99,14 +100,20 @@ import io.getstream.chat.android.ui.common.utils.MediaStringUtil
99100
* @param onCancelAction Handler for the cancel button on Message actions, such as Edit and Reply.
100101
* @param onLinkPreviewClick Handler when the user taps on a link preview.
101102
* @param onCancelLinkPreviewClick Handler when the user taps on the cancel link preview.
102-
* @param onUserSelected Handler when the user taps on a user suggestion item.
103+
* @param onUserSelected Legacy handler that fires only when the user taps a user suggestion item.
104+
* Kept for backward compatibility; new callers should use [onMentionSelected], which receives every
105+
* mention type including users. Note both callbacks fire on a user tap: a custom [onUserSelected]
106+
* runs in addition to [onMentionSelected], so the default [onMentionSelected] still inserts the
107+
* mention. To replace the default selection behavior, override [onMentionSelected].
103108
* @param onCommandSelected Handler for every tap on a command suggestion item, including taps on
104109
* disabled items. The default emits [MessageComposerViewEvent.CommandUnavailable] on
105110
* [MessageComposerViewModel.events] when the command is not available for the current action;
106111
* overrides replace that behavior and become responsible for their own filtering and feedback.
107112
* @param onAlsoSendToChannelChange Handler when the "Also send to channel" checkbox is changed.
108113
* @param onActiveCommandDismiss Called when the user taps the dismiss button on the active command chip.
109114
* @param recordingActions The actions that can be performed on an audio recording.
115+
* @param onMentionSelected Handler when the user taps any mention suggestion item. This is the
116+
* canonical callback for all mention selections.
110117
* @param input Customizable composable that represents the input field for the composer, [MessageInput] by default.
111118
*/
112119
@Composable
@@ -124,14 +131,15 @@ public fun MessageComposer(
124131
onCancelAction: () -> Unit = { viewModel.dismissMessageActions() },
125132
onLinkPreviewClick: ((LinkPreview) -> Unit)? = null,
126133
onCancelLinkPreviewClick: (() -> Unit)? = { viewModel.cancelLinkPreview() },
127-
onUserSelected: (User) -> Unit = { viewModel.selectMention(it) },
134+
onUserSelected: (User) -> Unit = {},
128135
onCommandSelected: (Command) -> Unit = viewModel::selectCommand,
129136
onAlsoSendToChannelChange: (Boolean) -> Unit = viewModel::setAlsoSendToChannel,
130137
onActiveCommandDismiss: () -> Unit = viewModel::clearActiveCommand,
131138
recordingActions: AudioRecordingActions = AudioRecordingActions.defaultActions(
132139
viewModel = viewModel,
133140
sendOnComplete = ChatTheme.config.composer.audioRecordingSendOnComplete,
134141
),
142+
onMentionSelected: (Mention) -> Unit = viewModel::selectMention,
135143
input: @Composable RowScope.(MessageComposerState) -> Unit = { state ->
136144
val inputFocusRequester = remember { FocusRequester() }
137145
LaunchedEffect(Unit) {
@@ -189,6 +197,7 @@ public fun MessageComposer(
189197
onSendMessage(messageWithData)
190198
},
191199
onUserSelected = onUserSelected,
200+
onMentionSelected = onMentionSelected,
192201
onCommandSelected = onCommandSelected,
193202
onAlsoSendToChannelSelected = onAlsoSendToChannelChange,
194203
onActiveCommandDismiss = onActiveCommandDismiss,
@@ -235,9 +244,16 @@ internal val LocalMessageComposerSnackbarHostState =
235244
* @param onCancelAction Handler for the cancel button on Message actions, such as Edit and Reply.
236245
* @param onLinkPreviewClick Handler when the user taps on a link preview.
237246
* @param onCancelLinkPreviewClick Handler when the user taps on the cancel link preview.
238-
* @param onUserSelected Handler when the user taps on a user suggestion item.
247+
* @param onUserSelected Legacy handler that fires only when the user taps a user suggestion item.
248+
* Kept for backward compatibility; new callers should use [onMentionSelected], which receives every
249+
* mention type including users. Note both callbacks fire on a user tap: a custom [onUserSelected]
250+
* runs in addition to [onMentionSelected], so the default [onMentionSelected] still inserts the
251+
* mention. To replace the default selection behavior, override [onMentionSelected].
239252
* @param onCommandSelected Handler when the user taps on a command suggestion item, including taps
240253
* on disabled items.
254+
* @param onMentionSelected Handler when the user taps any mention suggestion item. Canonical
255+
* callback for all mention selections. Must be wired when any non-user mention kind is enabled —
256+
* otherwise non-user mentions silently no-op on tap.
241257
* @param onAlsoSendToChannelChange Handler when the "Also send to channel" checkbox is changed.
242258
* @param onActiveCommandDismiss Called when the user taps the dismiss button on the active command chip.
243259
* @param recordingActions The actions that can be performed on an audio recording.
@@ -264,6 +280,7 @@ public fun MessageComposer(
264280
onAlsoSendToChannelChange: (Boolean) -> Unit = {},
265281
onActiveCommandDismiss: () -> Unit = {},
266282
recordingActions: AudioRecordingActions = AudioRecordingActions.None,
283+
onMentionSelected: (Mention) -> Unit = {},
267284
input: @Composable RowScope.(MessageComposerState) -> Unit = { state ->
268285
ChatTheme.componentFactory.MessageComposerInput(
269286
params = MessageComposerInputParams(
@@ -285,7 +302,13 @@ public fun MessageComposer(
285302
},
286303
) {
287304
val validationErrors = messageComposerState.validationErrors
288-
val userSuggestions = messageComposerState.mentionSuggestions
305+
// Prefer the list with all mentions but fall back to the legacy user-only field for
306+
// callers that construct MessageComposerState directly without going through the controller.
307+
val suggestedMentions = messageComposerState.suggestedMentions
308+
val legacyMentionSuggestions = messageComposerState.mentionSuggestions
309+
val mentionSuggestions = remember(suggestedMentions, legacyMentionSuggestions) {
310+
suggestedMentions.ifEmpty { legacyMentionSuggestions.map(Mention::User) }
311+
}
289312
val commandSuggestions = messageComposerState.commandSuggestions
290313
val snackbarHostState = LocalMessageComposerSnackbarHostState.current ?: remember { SnackbarHostState() }
291314

@@ -295,12 +318,13 @@ public fun MessageComposer(
295318
)
296319

297320
MessageComposerSurface(modifier = modifier) {
298-
if (userSuggestions.isNotEmpty()) {
299-
SuggestionsMenu(contentMaxHeight = UserSuggestionsMaxHeight) {
300-
UserSuggestionList(
301-
users = userSuggestions,
321+
if (mentionSuggestions.isNotEmpty()) {
322+
SuggestionsMenu(contentMaxHeight = MentionSuggestionsMaxHeight) {
323+
MentionSuggestionList(
324+
mentions = mentionSuggestions,
302325
currentUser = messageComposerState.currentUser,
303326
onUserSelected = onUserSelected,
327+
onMentionSelected = onMentionSelected,
304328
)
305329
}
306330
}
@@ -358,7 +382,7 @@ public fun MessageComposer(
358382
}
359383
}
360384

361-
private val UserSuggestionsMaxHeight = 176.dp
385+
private val MentionSuggestionsMaxHeight = 176.dp
362386
private val CommandSuggestionsMaxHeight = 208.dp
363387

364388
@StringRes
@@ -368,18 +392,21 @@ private fun MessageComposerViewEvent.messageResOrNull(): Int? = when (this) {
368392
is Reply -> R.string.stream_compose_message_composer_command_unavailable_in_reply
369393
else -> null
370394
}
395+
371396
is MessageComposerViewEvent.CancelCommandRequired -> when (action) {
372397
is Edit -> R.string.stream_compose_message_composer_cancel_command_to_edit
373398
is Reply -> R.string.stream_compose_message_composer_cancel_command_to_reply
374399
else -> null
375400
}
401+
376402
else -> null
377403
}
378404

379405
private fun MessageComposerViewEvent.snackbarVariant(): StreamSnackbarVariant = when (this) {
380406
is MessageComposerViewEvent.CommandUnavailable,
381407
is MessageComposerViewEvent.CancelCommandRequired,
382408
-> StreamSnackbarVariant.Error
409+
383410
else -> StreamSnackbarVariant.Default
384411
}
385412

@@ -516,6 +543,14 @@ internal fun MessageComposerFixedStyleWithUserSuggestions() {
516543
PreviewUserData.user1,
517544
PreviewUserData.user5,
518545
),
546+
suggestedMentions = listOf(
547+
Mention.Channel,
548+
Mention.Here,
549+
Mention.User(PreviewUserData.userWithOnlineStatus),
550+
Mention.User(PreviewUserData.user1),
551+
Mention.User(PreviewUserData.user5),
552+
Mention.Role("admin"),
553+
),
519554
),
520555
onSendMessage = { _, _ -> },
521556
)
@@ -622,6 +657,14 @@ internal fun MessageComposerFloatingStyleWithUserSuggestions() {
622657
PreviewUserData.user1,
623658
PreviewUserData.user5,
624659
),
660+
suggestedMentions = listOf(
661+
Mention.Channel,
662+
Mention.Here,
663+
Mention.User(PreviewUserData.userWithOnlineStatus),
664+
Mention.User(PreviewUserData.user1),
665+
Mention.User(PreviewUserData.user5),
666+
Mention.Role("admin"),
667+
),
625668
),
626669
onSendMessage = { _, _ -> },
627670
)

0 commit comments

Comments
 (0)