Add enhanced mention suggestions#1497
Conversation
Use a sibling stream-chat-swift checkout when present so unreleased LLC APIs (e.g. searchRoles) are available during local development, falling back to the released package otherwise.
Render dedicated cells for user, broadcast, role and group mentions and wire them into the suggestions container.
Add unit tests covering the mention suggestion model and configuration.
Replace continuation-based controller calls with UserSearch, RoleSearch, and UserGroupSearch, and update mention suggestion tests for async flow.
Move the mention suggestion types and logic to the core SDK and drive the composer through a MentionSuggestionsProvider. Remove the composer-level mention configuration, add a provider-based initializer to MentionsCommandHandler, and use the enhanced provider in the demo app.
Match mention suggestion variants by casting the wrapped suggestion value instead of switching over enum cases.
Use the renamed `kind` value and namespaced variant types when matching mention suggestions.
Drop the obsolete mention suggestions config assertions now that mention types are derived from the channel capabilities.
Temporarily resolve StreamChat from the add/enhanced-user-mentions LLC branch until those changes are released.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds enhanced composer mention suggestions for ChangesEnhanced Mention Suggestions Feature
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 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 |
Generated by 🚫 Danger |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift (1)
308-310:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHydrate all enhanced mention fields when pre-filling composer content.
fillEditedMessage/fillDraftMessageonly restorementionedUsers. The newly added role/group/here/channel state stays empty, so a subsequent draft update/send can drop mention metadata unless the user re-selects mentions.💡 Suggested fix
@@ text = message.text mentionedUsers = message.mentionedUsers + mentionedRoles = Set(message.mentionedRoles) + mentionedGroups = message.mentionedGroups + mentionsHere = message.mentionedHere + mentionsChannel = message.mentionedChannel showReplyInChannel = message.showReplyInChannel @@ text = message.text mentionedUsers = message.mentionedUsers + mentionedRoles = Set(message.mentionedRoles) + mentionedGroups = message.mentionedGroups + mentionsHere = message.mentionedHere + mentionsChannel = message.mentionedChannel quotedMessage?.wrappedValue = message.quotedMessageAlso applies to: 323-326
🤖 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 `@Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift` around lines 308 - 310, The fillEditedMessage and fillDraftMessage methods currently only restore the mentionedUsers field when pre-filling the composer, but fail to restore the newly added enhanced mention state fields (such as role, group, here, and channel mention flags). Add assignments in both methods to restore all enhanced mention fields from the message object to their corresponding view model properties, ensuring complete mention metadata is preserved and not dropped on subsequent updates or sends.
🧹 Nitpick comments (1)
StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift (1)
207-207: ⚡ Quick winUse
AssertSnapshotfor this new snapshot test.This file is under
StreamChatSwiftUITests/Tests/**, and the new assertion should use the test helper wrapper for consistency with project conventions.🔧 Suggested change
- assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + AssertSnapshot(view)As per coding guidelines: "Prefer using
AssertSnapshotfrom StreamChatTestHelpers instead of the SnapshotTesting framework directly for snapshot tests."🤖 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 `@StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift` at line 207, The snapshot assertion in the MessageView_Tests file is using the direct assertSnapshot call from SnapshotTesting framework instead of the AssertSnapshot test helper wrapper. Replace the assertSnapshot function call (which takes the parameters matching: view and as: .image(perceptualPrecision: precision)) with the AssertSnapshot test helper to maintain consistency with project conventions and coding guidelines for snapshot tests in the StreamChatSwiftUITests directory.Source: Coding guidelines
🤖 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.
Inline comments:
In `@CHANGELOG.md`:
- Around line 6-7: The CHANGELOG.md file under the `# Upcoming` section uses
non-standard subsection headings. Replace the current `### ✅ Added` heading with
`### Added` (removing the emoji) and add the missing sibling subsections `###
Fixed` and `### Changed` to match the Keep a Changelog format exactly.
Reorganize the existing content under the appropriate subsection based on its
nature.
In `@Package.swift`:
- Around line 19-22: The temporary branch reference `add/enhanced-user-mentions`
in the stream-chat-swift package dependency is not pinned to a specific
revision, which causes non-deterministic builds as the branch can be updated
upstream. Replace the branch parameter with a revision parameter set to a
specific commit hash (e.g., `8ed426cc1808be079192143fd866bf984989d97a`) in the
.package declaration for the stream-chat-swift dependency in Package.swift.
Additionally, ensure the same commit hash revision is also updated in the Xcode
project configuration file (StreamChatSwiftUI.xcodeproj/project.pbxproj) to
maintain consistency across both dependency management systems.
In
`@Sources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionsCommandHandler.swift`:
- Around line 132-137: In the Future closure within the method that handles
typing mentions, when the guard let self check fails, the early return statement
exits without resolving the promise, leaving subscribers waiting indefinitely.
Fix this by calling unsafePromise with either a failure result or a default
empty success value (such as SuggestionInfo with an empty suggestions array)
before the early return, ensuring the promise is always resolved regardless of
self's lifecycle state.
- Around line 28-44: The convenience initializer in MentionsCommandHandler is
accepting a userSearchController parameter but silently ignoring it, which
changes runtime behavior for existing integrators. Either pass the
userSearchController parameter to the DefaultMentionSuggestionsProvider
initialization if it supports it, or mark the userSearchController parameter as
deprecated using the `@available`(*, deprecated, message:) attribute to warn users
that it is no longer used while maintaining source compatibility.
In
`@Sources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionSuggestionsView.swift`:
- Around line 38-44: The accessibility hint in the MentionSuggestionsView is
using a hardcoded user-specific localization string
(L10n.Composer.Suggestions.User.accessibilityLabel) with an empty label
argument, which produces incorrect VoiceOver output for non-user mentions like
`@here`, `@channel`, roles, and groups. Fix this by determining the actual
suggestion type (user, special mention, role, group, etc.) and selecting the
appropriate localization string based on that type, then pass the actual item
name or label instead of an empty string as the first argument to ensure
VoiceOver correctly identifies each mention type.
In `@Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings`:
- Around line 234-238: The accessibility announcement string for the key
"composer.suggestions.mentions.accessibility-announcement" currently says "User
list, %d results" but this is inaccurate since the suggestion types have
expanded to include channel, here, role, and group mention options in addition
to users. Update this string to reflect a more generic description of the
suggestions list that accurately conveys what the user will hear when these
expanded suggestion types appear, rather than specifically referencing just a
user list.
---
Outside diff comments:
In `@Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift`:
- Around line 308-310: The fillEditedMessage and fillDraftMessage methods
currently only restore the mentionedUsers field when pre-filling the composer,
but fail to restore the newly added enhanced mention state fields (such as role,
group, here, and channel mention flags). Add assignments in both methods to
restore all enhanced mention fields from the message object to their
corresponding view model properties, ensuring complete mention metadata is
preserved and not dropped on subsequent updates or sends.
---
Nitpick comments:
In `@StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift`:
- Line 207: The snapshot assertion in the MessageView_Tests file is using the
direct assertSnapshot call from SnapshotTesting framework instead of the
AssertSnapshot test helper wrapper. Replace the assertSnapshot function call
(which takes the parameters matching: view and as: .image(perceptualPrecision:
precision)) with the AssertSnapshot test helper to maintain consistency with
project conventions and coding guidelines for snapshot tests in the
StreamChatSwiftUITests directory.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9466f3d2-d845-4e56-befd-53753880b6a3
⛔ Files ignored due to path filters (10)
Sources/StreamChatSwiftUI/Generated/L10n.swiftis excluded by!**/generated/**StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_liquidGlassStyle.default-light.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_liquidGlassStyle.extraExtraExtraLarge-light.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_liquidGlassStyle.rightToLeftLayout-default.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_liquidGlassStyle.small-dark.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_regularStyle.default-light.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_regularStyle.extraExtraExtraLarge-light.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_regularStyle.rightToLeftLayout-default.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/__Snapshots__/MentionSuggestionsView_Tests/test_mentionSuggestionsView_regularStyle.small-dark.pngis excluded by!**/*.pngStreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageView_Tests/test_messageViewTextMentionAllTypes_snapshot.1.pngis excluded by!**/*.png
📒 Files selected for processing (21)
CHANGELOG.mdDemoAppSwiftUI/AppConfiguration/AppConfiguration.swiftDemoAppSwiftUI/AppDelegate.swiftPackage.swiftSources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelExtensions.swiftSources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swiftSources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionSuggestionsView.swiftSources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionsCommandHandler.swiftSources/StreamChatSwiftUI/ChatComposer/Suggestions/SuggestionsContainerView.swiftSources/StreamChatSwiftUI/Resources/en.lproj/Localizable.stringsSources/StreamChatSwiftUI/Utils/Common/ChatMessage+Extensions.swiftStreamChatSwiftUI.xcodeproj/project.pbxprojStreamChatSwiftUITests/Infrastructure/Mocks/ChatMessageControllerSUI_Mock.swiftStreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/CommandsHandler_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionSuggestion_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionSuggestionsView_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionsCommandHandler_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MuteCommandHandler_Tests.swiftStreamChatSwiftUITests/Tests/ChatChannel/Suggestions/TestCommandsConfig.swift
💤 Files with no reviewable changes (2)
- StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/TestCommandsConfig.swift
- Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelExtensions.swift
martinmitrevski
left a comment
There was a problem hiding this comment.
lgtm - just left few smaller comments
17fd69d to
671c7a2
Compare
Public Interface+ public struct MentionSuggestionView: View
+
+ public var body: some View
+
+
+ public init(factory: Factory = DefaultViewFactory.shared,suggestion: MentionSuggestion,suggestionSelected: @escaping (MentionSuggestion) -> Void)
+ public struct MentionSuggestionsView: View
+
+ public var body: some View
+
+
+ public init(factory: Factory = DefaultViewFactory.shared,suggestions: [MentionSuggestion],suggestionSelected: @escaping (MentionSuggestion) -> Void)
@MainActor open class MessageComposerViewModel: ObservableObject
- public var canSendMessage: Bool
+ public var mentionedRoles
- public var hasContent: Bool
+ public var mentionedGroups
- public var composerInputState: MessageComposerInputState
+ public var mentionsHere
- public var shouldShowRecordingGestureOverlay: Bool
+ public var mentionsChannel
- public var sendInChannelShown: Bool
+ public var canSendMessage: Bool
- public var isDirectChannel: Bool
+ public var hasContent: Bool
- public var showSuggestionsOverlay: Bool
+ public var composerInputState: MessageComposerInputState
-
+ public var shouldShowRecordingGestureOverlay: Bool
-
+ public var sendInChannelShown: Bool
- public init(channelController: ChatChannelController,messageController: ChatMessageController?,eventsController: EventsController? = nil,quotedMessage: Binding<ChatMessage?>? = nil,editedMessage: Binding<ChatMessage?>? = nil,willSendMessage: (() -> Void)? = nil)
+ public var isDirectChannel: Bool
-
+ public var showSuggestionsOverlay: Bool
-
+
- public func addFileURLs(_ urls: [URL])
+
- public func fillEditedMessage(_ editedMessage: ChatMessage?)
+ public init(channelController: ChatChannelController,messageController: ChatMessageController?,eventsController: EventsController? = nil,quotedMessage: Binding<ChatMessage?>? = nil,editedMessage: Binding<ChatMessage?>? = nil,willSendMessage: (() -> Void)? = nil)
- public func fillDraftMessage()
+
- public func updateDraftMessage(quotedMessage: ChatMessage?,isSilent: Bool = false,extraData: [String: RawJSON] = [:])
+
- public func deleteDraftMessage()
+ public func addFileURLs(_ urls: [URL])
- open func sendMessage(isSilent: Bool = false,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: (@MainActor () -> Void)? = nil)
+ public func fillEditedMessage(_ editedMessage: ChatMessage?)
- public func change(pickerState: AttachmentPickerState)
+ public func fillDraftMessage()
- public func imageTapped(_ addedAsset: AddedAsset)
+ public func updateDraftMessage(quotedMessage: ChatMessage?,isSilent: Bool = false,extraData: [String: RawJSON] = [:])
- public func imagePasted(_ image: UIImage)
+ public func deleteDraftMessage()
- public func removeAttachment(with id: String)
+ open func sendMessage(isSilent: Bool = false,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: (@MainActor () -> Void)? = nil)
- public func cameraImageAdded(_ image: AddedAsset)
+ public func change(pickerState: AttachmentPickerState)
- public func isImageSelected(with id: String)-> Bool
+ public func imageTapped(_ addedAsset: AddedAsset)
- public func customAttachmentTapped(_ attachment: CustomAttachment)
+ public func imagePasted(_ image: UIImage)
- public func isCustomAttachmentSelected(_ attachment: CustomAttachment)-> Bool
+ public func removeAttachment(with id: String)
- public func askForPhotosPermission()
+ public func cameraImageAdded(_ image: AddedAsset)
- public func handleCommand(for text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,extraData: [String: Any])
+ public func isImageSelected(with id: String)-> Bool
- open func convertAddedAssetsToPayloads()throws -> [AnyAttachmentPayload]
+ public func customAttachmentTapped(_ attachment: CustomAttachment)
- public func checkForMentionedUsers(commandId: String?,extraData: [String: Any])
+ public func isCustomAttachmentSelected(_ attachment: CustomAttachment)-> Bool
- public func clearRemovedMentions()
+ public func askForPhotosPermission()
- public func clearInputData()
+ public func handleCommand(for text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,extraData: [String: Any])
- public func checkChannelCooldown()
+ open func convertAddedAssetsToPayloads()throws -> [AnyAttachmentPayload]
- public func updateAddedAssets(_ assets: [AddedAsset])
+ public func checkForMentionedUsers(commandId: String?,extraData: [String: Any])
+ public func clearRemovedMentions()
+ public func clearInputData()
+ public func checkChannelCooldown()
+ public func updateAddedAssets(_ assets: [AddedAsset])
public final class MentionsCommandHandler: CommandHandler
- public init(channelController: ChatChannelController,userSearchController: ChatUserSearchController? = nil,commandSymbol: String,mentionAllAppUsers: Bool,id: String = "mentions")
+ public convenience init(channelController: ChatChannelController,userSearchController: ChatUserSearchController? = nil,commandSymbol: String,mentionAllAppUsers: Bool,id: String = "mentions")
-
+ public init(channelController: ChatChannelController,commandSymbol: String,provider: MentionSuggestionsProvider? = nil,id: String = "mentions")
-
+
- public func canHandleCommand(in text: String,caretLocation: Int)-> ComposerCommand?
+
- public func handleCommand(for text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,extraData: [String: Any])
+ public func canHandleCommand(in text: String,caretLocation: Int)-> ComposerCommand?
- public func commandHandler(for command: ComposerCommand)-> CommandHandler?
+ public func handleCommand(for text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,extraData: [String: Any])
- public func showSuggestions(for command: ComposerCommand)-> Future<SuggestionInfo, Error>
+ public func commandHandler(for command: ComposerCommand)-> CommandHandler?
+ public func showSuggestions(for command: ComposerCommand)-> Future<SuggestionInfo, Error> |
This reverts commit cb5ac25.
Move the enhanced mentions commands config from the DemoApp into the SDK as EnhancedCommandsConfig so customers can use it directly.
This reverts commit 45a9805.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@Sources/StreamChatSwiftUI/ChatComposer/Suggestions/CommandsConfig.swift`:
- Line 89: The renaming of the public class from EnhancedCommandsConfig to
EnhancedMentionsCommandsConfig introduces a source-breaking change. Add a
deprecated typealias with the old name EnhancedCommandsConfig that points to the
new EnhancedMentionsCommandsConfig at the same scope level, and mark it with the
appropriate deprecation warning annotation to inform users of the API change
while maintaining backward compatibility for existing code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8907d4eb-9758-4208-b5a6-c1768b849b89
📒 Files selected for processing (2)
DemoAppSwiftUI/AppConfiguration/AppConfiguration.swiftSources/StreamChatSwiftUI/ChatComposer/Suggestions/CommandsConfig.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- DemoAppSwiftUI/AppConfiguration/AppConfiguration.swift
SDK Size
|
StreamChatSwiftUI XCSize
Show 51 more objects
|
🔗 Issue Links
🎯 Goal
Bring the enhanced mentions experience to the SwiftUI SDK: in addition to user mentions, the composer can now suggest and render
@here,@channel, role and group mentions.📝 Summary
MentionSuggestionsProviderfrom the LLC inMentionsCommandHandler.MentionSuggestionsViewrendering a dedicated cell per mention type (user, here, channel, role, group).🛠 Implementation
MentionSuggestion/MentionTypeare now extensible value types in the LLC, so the view switches oversuggestion.kindto build each cell. The composer view model mirrors the LLC's send/draft paths for the new mention types. Mention suggestion logic and configuration live in the LLC behindMentionSuggestionsProvider, so SwiftUI only wires the provider and renders results.🧪 Manual Testing Notes
In the demo app, open a channel and type
@to see user/here/channel suggestions, and@followed by text to see users, roles and groups. Send the message and verify the mentions are highlighted.☑️ Contributor Checklist
docs-contentrepoSummary by CodeRabbit
@here,@channel, roles, and groups in addition to individual users.@here,@channel, roles, and groups.