Add composer autocomplete for enhanced mentions in Compose SDK#6503
Add composer autocomplete for enhanced mentions in Compose SDK#6503gpunto wants to merge 1 commit into
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
7aa6cf5 to
6e8179d
Compare
6e8179d to
e9c80ac
Compare
e9c80ac to
612a70a
Compare
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
WalkthroughExtends the message composer to support unified mention suggestions beyond users: channels, ChangesUnified Mention Suggestions
Sequence Diagram(s)sequenceDiagram
participant User
participant MessageComposer
participant MessageComposerController
participant MentionLookupHandler
participant ChatClient
User->>MessageComposer: types `@query`
MessageComposer->>MessageComposerController: input change (debounced)
MessageComposerController->>MentionLookupHandler: handleMentionLookup(query)
par concurrent fetches
MentionLookupHandler->>ChatClient: searchRoles(query)
MentionLookupHandler->>ChatClient: searchUserGroups(query, team?)
MentionLookupHandler->>MentionLookupHandler: userLookupHandler(query)
end
MentionLookupHandler-->>MessageComposerController: List~Mention~ [channel, here, roles, groups, users]
MessageComposerController-->>MessageComposer: state(suggestedMentions, mentionSuggestions)
MessageComposer->>MessageComposer: render MentionSuggestionList
User->>MessageComposer: selects mention
MessageComposer->>MessageComposerController: onMentionSelected(mention)
MessageComposerController->>MessageComposerController: filterMentions → FilteredMentions
MessageComposerController-->>MessageComposer: buildNewMessage with mention fields
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/LocalUserLookupHandler.kt (1)
44-63:⚠️ Potential issue | 🟠 MajorRe-throw coroutine cancellation in user lookup.
Wrapping the function in
withContext(DispatcherProvider.IO)introduces a cancellation point. However, the broadcatch (e: Exception)on line 60 catchesCancellationExceptionas well, preventing cancellation from propagating. This allows canceled lookup operations to returnemptyList()instead of propagating the cancellation, potentially causing stale results to race newer UI updates.Suggested fix
+import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withContext @@ - } catch (e: Exception) { + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { logger.e(e) { "[handleUserLookup] failed: $e" } emptyList() }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/LocalUserLookupHandler.kt` around lines 44 - 63, In the handleUserLookup function, modify the catch block to re-throw CancellationException after catching it. The broad catch (e: Exception) block currently catches CancellationException which prevents coroutine cancellation from propagating properly. Add a check inside the catch block to detect if the caught exception is a CancellationException, and if so, re-throw it immediately before logging and returning emptyList() for other exception types. This ensures that when the lookup operation is cancelled, the cancellation propagates correctly instead of returning stale results.
🧹 Nitpick comments (2)
stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/MentionSuggestionListTest.kt (1)
40-47: ⚡ Quick winAdd a group mention row to this snapshot.
This baseline covers channel/here/user/role, but not
Mention.Group, which is part of the new mention surface.Suggested patch
import io.getstream.chat.android.previewdata.PreviewUserData +import io.getstream.chat.android.models.UserGroup import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention @@ mentions = listOf( Mention.Channel, Mention.Here, Mention.User(PreviewUserData.user1), Mention.User(PreviewUserData.user2), Mention.User(PreviewUserData.user3), Mention.Role("admin"), + Mention.Group(UserGroup(id = "g1", name = "platform")), ), )As per coding guidelines
stream-chat-android-compose/**/*{Test,Snapshot}.kt: add Paparazzi snapshots for Compose UI regressions and runverifyPaparazziDebug.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/MentionSuggestionListTest.kt` around lines 40 - 47, The mentions list in MentionSuggestionListTest.kt is missing a Mention.Group entry, which is part of the new mention surface. Add a Mention.Group instance to the mentions list alongside the existing Mention.Channel, Mention.Here, Mention.User, and Mention.Role entries to ensure the snapshot test covers all mention types.Source: Coding guidelines
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionLookupHandler.kt (1)
46-78: 💤 Low valueConsider extracting helper functions to reduce cognitive complexity.
SonarCloud flags this method's cognitive complexity at 18 (limit 15). While the logic is correct and readable, extracting the capability-gated async launches and the
buildListassembly into small private helpers would lower the metric and improve testability.♻️ Suggested refactor
suspend fun handleMentionLookup(query: String): List<Mention> = coroutineScope { val capabilities = channelState.value?.channelData?.value?.ownCapabilities.orEmpty() - val getGroups = async { - if (ChannelCapabilities.NOTIFY_GROUP in capabilities) searchGroups(query) else emptyList() - } - val getUsers = async { - if (ChannelCapabilities.CREATE_MENTION in capabilities) { - userLookupHandler.handleUserLookup(query) - } else { - emptyList() - } - } - val getRoles = async { - if (ChannelCapabilities.NOTIFY_ROLE in capabilities) searchRoles(query) else emptyList() - } - - buildList { - if (ChannelCapabilities.NOTIFY_CHANNEL in capabilities && - Mention.Channel.display.matchesMentionQuery(query) - ) { - add(Mention.Channel) - } - if (ChannelCapabilities.NOTIFY_HERE in capabilities && - Mention.Here.display.matchesMentionQuery(query) - ) { - add(Mention.Here) - } - - getRoles.await().forEach { add(Mention.Role(it)) } - getGroups.await().forEach { add(Mention.Group(it)) } - getUsers.await().forEach { add(Mention.User(it)) } - } + val (roles, groups, users) = fetchMentionSources(capabilities, query) + assembleMentions(capabilities, query, roles, groups, users) } + +private suspend fun CoroutineScope.fetchMentionSources( + capabilities: Set<String>, + query: String, +): Triple<List<String>, List<UserGroup>, List<User>> { + val getRoles = async { + if (ChannelCapabilities.NOTIFY_ROLE in capabilities) searchRoles(query) else emptyList() + } + val getGroups = async { + if (ChannelCapabilities.NOTIFY_GROUP in capabilities) searchGroups(query) else emptyList() + } + val getUsers = async { + if (ChannelCapabilities.CREATE_MENTION in capabilities) { + userLookupHandler.handleUserLookup(query) + } else { + emptyList() + } + } + return Triple(getRoles.await(), getGroups.await(), getUsers.await()) +} + +private fun assembleMentions( + capabilities: Set<String>, + query: String, + roles: List<String>, + groups: List<UserGroup>, + users: List<User>, +): List<Mention> = buildList { + if (ChannelCapabilities.NOTIFY_CHANNEL in capabilities && + Mention.Channel.display.matchesMentionQuery(query) + ) add(Mention.Channel) + if (ChannelCapabilities.NOTIFY_HERE in capabilities && + Mention.Here.display.matchesMentionQuery(query) + ) add(Mention.Here) + roles.forEach { add(Mention.Role(it)) } + groups.forEach { add(Mention.Group(it)) } + users.forEach { add(Mention.User(it)) } +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionLookupHandler.kt` around lines 46 - 78, The handleMentionLookup method has cognitive complexity of 18, exceeding the limit of 15. Extract two private helper functions to reduce complexity: first, create a helper function to manage the three async operations (getGroups, getUsers, getRoles launches) that returns their results, and second, create another helper function that takes those async results and the query parameter to build and return the final list of Mention objects using the buildList logic. This refactoring will lower cognitive complexity and improve testability while keeping the handleMentionLookup method as a coordinator that calls these helpers.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/LocalUserLookupHandler.kt`:
- Around line 44-63: In the handleUserLookup function, modify the catch block to
re-throw CancellationException after catching it. The broad catch (e: Exception)
block currently catches CancellationException which prevents coroutine
cancellation from propagating properly. Add a check inside the catch block to
detect if the caught exception is a CancellationException, and if so, re-throw
it immediately before logging and returning emptyList() for other exception
types. This ensures that when the lookup operation is cancelled, the
cancellation propagates correctly instead of returning stale results.
---
Nitpick comments:
In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/MentionSuggestionListTest.kt`:
- Around line 40-47: The mentions list in MentionSuggestionListTest.kt is
missing a Mention.Group entry, which is part of the new mention surface. Add a
Mention.Group instance to the mentions list alongside the existing
Mention.Channel, Mention.Here, Mention.User, and Mention.Role entries to ensure
the snapshot test covers all mention types.
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionLookupHandler.kt`:
- Around line 46-78: The handleMentionLookup method has cognitive complexity of
18, exceeding the limit of 15. Extract two private helper functions to reduce
complexity: first, create a helper function to manage the three async operations
(getGroups, getUsers, getRoles launches) that returns their results, and second,
create another helper function that takes those async results and the query
parameter to build and return the final list of Mention objects using the
buildList logic. This refactoring will lower cognitive complexity and improve
testability while keeping the handleMentionLookup method as a coordinator that
calls these helpers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 38f1b34c-edfb-4955-98e0-aae5f89339bd
⛔ Files ignored due to path filters (4)
stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_MentionSuggestionListTest_mention_suggestion_list.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_fixed_style_with_user_suggestions.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style_with_user_suggestions_in_dark_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style_with_user_suggestions_in_light_mode.pngis excluded by!**/*.png
📒 Files selected for processing (35)
stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.ktstream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.ktstream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/MentionSuggestionItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/UserSuggestionItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/UserSuggestionList.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.ktstream-chat-android-compose/src/main/res/values-es/strings.xmlstream-chat-android-compose/src/main/res/values-fr/strings.xmlstream-chat-android-compose/src/main/res/values-hi/strings.xmlstream-chat-android-compose/src/main/res/values-in/strings.xmlstream-chat-android-compose/src/main/res/values-it/strings.xmlstream-chat-android-compose/src/main/res/values-ja/strings.xmlstream-chat-android-compose/src/main/res/values-ko/strings.xmlstream-chat-android-compose/src/main/res/values/strings.xmlstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/MentionSuggestionListTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.ktstream-chat-android-core/api/stream-chat-android-core.apistream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChannelCapabilities.ktstream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/MessageComposer.ktstream-chat-android-ui-common/api/stream-chat-android-ui-common.apistream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/LocalUserLookupHandler.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionLookupHandler.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/query/filter/DefaultUserQueryFilter.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState.ktstream-chat-android-ui-common/src/main/res/drawable/stream_design_ic_megaphone.xmlstream-chat-android-ui-common/src/main/res/drawable/stream_design_ic_role.xmlstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionLookupHandlerTest.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/query/filter/DefaultUserQueryFilterTest.ktstream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt
💤 Files with no reviewable changes (1)
- stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/UserSuggestionItem.kt
andremion
left a comment
There was a problem hiding this comment.
Good job!
One small heads-up: DefaultUserQueryFilter now does word-prefix matching and sorts alphabetically, instead of substring matching sorted by match position. That seems intentional and matches the spec, so no change needed, but it does change existing user-mention results, so maybe we should call it out somewhere for integrators.
Also, left two small comments.
| onLinkPreviewClick: ((LinkPreview) -> Unit)? = null, | ||
| onCancelLinkPreviewClick: (() -> Unit)? = { viewModel.cancelLinkPreview() }, | ||
| onUserSelected: (User) -> Unit = { viewModel.selectMention(it) }, | ||
| onUserSelected: (User) -> Unit = {}, |
There was a problem hiding this comment.
The default for onUserSelected moved from { viewModel.selectMention(it) } to {}, and onMentionSelected now defaults to viewModel::selectMention. For a caller who passes a custom onUserSelected but no onMentionSelected, the default selectMention still runs, so for user taps both their handler and selectMention fire. With selectedMentions being a Set and the text insert being idempotent this is harmless in the common case, but a caller who overrode onUserSelected to replace or suppress the default selection would now get the mention inserted anyway. Is that intended? Might be worth a note for integrators who customized onUserSelected.
| is Mention.User -> "user:${mention.user.id}" | ||
| is Mention.Channel -> "channel" | ||
| is Mention.Here -> "here" | ||
| is Mention.Role -> "role:${mention.role}" |
There was a problem hiding this comment.
itemsIndexed throws if two items produce the same key. Roles key on name (role:<name>), so if searchRoles ever returned two entries with the same name the list would crash. Role names are unique server-side so this is low risk, but a distinct() on the role results (or folding the index into the key) would remove the chance entirely. Worth guarding?
There was a problem hiding this comment.
I just noticed that this user gate looks risky for Permissions V1 apps.
By default on V1, regular members don't get create-mention in own_capabilities (only admins do). But the user mentions work fine there because the server enforces create-mention on send only under V2. So gating user suggestions on it here would hide all user-mention suggestions for regular users on every V1 app, which is a regression from the current always-show behavior.
The iOS is keeping user suggestions ungated for exactly this reason.
Maybe we could drop the create-mention gate for users (matching the other SDKs), or only apply it when we know the channel is on V2?
The @channel/@here/role/group gates are fine; the issue might be with the user gate.



Goal
Offer
@channel,@here, role, and user-group suggestions in the Compose composer alongside user mentions, gated by channel capabilities.This is PR 4 in the enhanced-mentions series.
Part of AND-1175
Implementation
ChannelCapabilitiesgate each mention type:CREATE_MENTION,NOTIFY_CHANNEL,NOTIFY_HERE,NOTIFY_ROLE,NOTIFY_GROUP.MentionLookupHandleraggregates users, roles, and groups for a given query.UserSuggestionItembecomesMentionSuggestionItem, rendering every mention type; exposed as aChatComponentFactoryslot.UI Changes
Testing
MentionLookupHandlerTestcovers capability gating, ordering, query matching, and empty paths.MessageComposerControllerTestcovers the newfilterMentionssurvival rules across every mention type.MessageComposerViewModelTestexercises the suggestion-list flow end to end.MentionSuggestionListTestbaseline; refreshedMessageComposerTestsnapshots that now render the broader suggestion item.Manual (on a channel with the new capabilities granted, e.g. any messaging channel in the demo app):
@and confirm@channeland@hereappear at the top.Send.Summary by CodeRabbit
New Features
@channel,@here, role-based, and group mentions alongside user mentions.Documentation