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.
π WalkthroughWalkthroughAdds ChangesEnhanced Mention Suggestions Feature
Sequence Diagram(s)sequenceDiagram
participant User
participant MentionSuggestionsView
participant SuggestionsContainerView
participant MessageComposerViewModel
participant MentionsCommandHandler
participant MentionSuggestionsProvider
User->>MessageComposerViewModel: types "`@ad`..."
MessageComposerViewModel->>MentionsCommandHandler: showSuggestions(for: "ad")
MentionsCommandHandler->>MentionSuggestionsProvider: mentionSuggestions(MentionSuggestionsRequest)
MentionSuggestionsProvider-->>MentionsCommandHandler: [MentionSuggestion] (users, roles, groups, here, channel)
MentionsCommandHandler-->>MessageComposerViewModel: Future([MentionSuggestion])
MessageComposerViewModel->>SuggestionsContainerView: suggestions["mentions"] = [MentionSuggestion]
SuggestionsContainerView->>MentionSuggestionsView: render suggestions list
User->>MentionSuggestionsView: taps suggestion
MentionSuggestionsView->>MessageComposerViewModel: handleCommand(["mentionSuggestion": suggestion])
MessageComposerViewModel->>MessageComposerViewModel: checkForMentionedUsers β update mentionedRoles/Groups/Here/Channel
MessageComposerViewModel->>MessageComposerViewModel: sendMessage β createNewMessage(mentionedRoles:mentionedGroupIds:mentionsHere:mentionsChannel:)
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 |
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
| ### β Added | ||
| - Add enhanced mention suggestions for `@here`, `@channel`, roles and groups in the composer [#1497](https://github.com/GetStream/stream-chat-swiftui/pull/1497) |
There was a problem hiding this comment.
Align # Upcoming subsections with the required changelog format.
Please use Keep a Changelog subsection names exactly (### Added, ### Fixed, ### Changed) under # Upcoming; the current ### β
Added heading and missing sibling subsections do not match the required structure.
Suggested structure
-### β
Added
+### Added
- Add enhanced mention suggestions for `@here`, `@channel`, roles and groups in the composer [`#1497`](https://github.com/GetStream/stream-chat-swiftui/pull/1497)
+
+### Fixed
+
+### ChangedAs per coding guidelines, update CHANGELOG.md under the # Upcoming section using Keep a Changelog format with ### Added, ### Fixed, and ### Changed subsections.
π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### β Added | |
| - Add enhanced mention suggestions for `@here`, `@channel`, roles and groups in the composer [#1497](https://github.com/GetStream/stream-chat-swiftui/pull/1497) | |
| ### Added | |
| - Add enhanced mention suggestions for `@here`, `@channel`, roles and groups in the composer [`#1497`](https://github.com/GetStream/stream-chat-swiftui/pull/1497) | |
| ### Fixed | |
| ### Changed |
π§° Tools
πͺ markdownlint-cli2 (0.22.1)
[warning] 6-6: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
π€ 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 `@CHANGELOG.md` around lines 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.
Source: Coding guidelines
| // Temporarily points to the enhanced mentions LLC branch until the | ||
| // corresponding stream-chat-swift changes are released. | ||
| .package(url: "https://github.com/GetStream/stream-chat-swift.git", branch: "add/enhanced-user-mentions") | ||
| ], |
There was a problem hiding this comment.
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
echo "Package.swift dependency:"
rg -n 'stream-chat-swift|branch|revision' Package.swift
echo
echo "Xcode project dependency:"
rg -n 'XCRemoteSwiftPackageReference "stream-chat-swift"|kind = branch|branch =|revision' StreamChatSwiftUI.xcodeproj/project.pbxproj
echo
echo "Current remote HEAD for add/enhanced-user-mentions:"
git ls-remote https://github.com/GetStream/stream-chat-swift.git refs/heads/add/enhanced-user-mentionsRepository: GetStream/stream-chat-swiftui
Length of output: 2541
Pin the temporary branch dependency to an immutable revision before release.
Both Package.swift (line 21) and StreamChatSwiftUI.xcodeproj/project.pbxproj (lines 1538-1539) currently point to the moving add/enhanced-user-mentions branch. Using a branch reference makes builds non-deterministic and can silently pull upstream changes. Lock both configurations to a specific commit hash (e.g., 8ed426cc1808be079192143fd866bf984989d97a).
Suggested change
- .package(url: "https://github.com/GetStream/stream-chat-swift.git", branch: "add/enhanced-user-mentions")
+ .package(url: "https://github.com/GetStream/stream-chat-swift.git", revision: "8ed426cc1808be079192143fd866bf984989d97a")Ensure the same revision is also set in the Xcode project configuration.
π€ 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 `@Package.swift` around lines 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.
| public convenience init( | ||
| channelController: ChatChannelController, | ||
| userSearchController: ChatUserSearchController? = nil, | ||
| commandSymbol: String, | ||
| mentionAllAppUsers: Bool, | ||
| id: String = "mentions" | ||
| ) { | ||
| self.init( | ||
| channelController: channelController, | ||
| commandSymbol: commandSymbol, | ||
| provider: DefaultMentionSuggestionsProvider( | ||
| client: channelController.client, | ||
| mentionAllAppUsers: mentionAllAppUsers | ||
| ), | ||
| id: id | ||
| ) | ||
| } |
There was a problem hiding this comment.
userSearchController is now silently ignored in the compatibility initializer.
This keeps source compatibility but changes runtime behavior for existing integrators that passed a custom search controller.
Suggested direction
+@available(*, deprecated, message: "Use init(channelController:commandSymbol:provider:id:) to customize mention suggestions.")
public convenience init(
channelController: ChatChannelController,
userSearchController: ChatUserSearchController? = nil,
commandSymbol: String,
mentionAllAppUsers: Bool,
id: String = "mentions"
) {
+ precondition(
+ userSearchController == nil,
+ "userSearchController is no longer used. Pass a custom MentionSuggestionsProvider instead."
+ )
self.init(
channelController: channelController,
commandSymbol: commandSymbol,
provider: DefaultMentionSuggestionsProvider(
client: channelController.client,
mentionAllAppUsers: mentionAllAppUsers
),
id: id
)
}As per coding guidelines: "Avoid source-breaking changes; add deprecations instead when modifying public APIs."
π€ 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/Suggestions/Mentions/MentionsCommandHandler.swift`
around lines 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.
Source: Coding guidelines
There was a problem hiding this comment.
this is a valid one
There was a problem hiding this comment.
@martinmitrevski, glad you agree! Let me plan the fix β I'll add the @available(*, deprecated, ...) attribute to the convenience initializer. I'll skip the precondition from the original suggestion since that would crash at runtime for existing integrators who already pass a userSearchController, which would itself be a source-breaking change.
- Add
@available(*, deprecated, message:)to theuserSearchControllerconvenience initializer inMentionsCommandHandler
π§ Learnings used
Learnt from: testableapple
Repo: GetStream/stream-chat-swiftui PR: 1206
File: Sources/StreamChatSwiftUI/Utils/Common/InputTextView.swift:116-125
Timestamp: 2026-02-17T14:14:43.490Z
Learning: In Swift UIKit code, when constraining a subview's leading and trailing anchors using directionalLayoutMargins, additionally pin the subview's widthAnchor to layoutMarginsGuide.widthAnchor to ensure RTL support. This pattern leverages layoutMarginsGuide (which respects layout direction) instead of raw leading/trailing anchors. Apply this guideline to Swift files under Sources/StreamChatSwiftUI (and similar UIKit constraint code) to improve RTL compatibility.
Failed to handle agent chat message. Please try again.
| return Future { [weak self] promise in | ||
| guard let self else { return } | ||
| nonisolated(unsafe) let unsafePromise = promise | ||
| Task { @MainActor in | ||
| let suggestions = await self.makeSuggestions(for: typingMention) | ||
| unsafePromise(.success(SuggestionInfo(key: id, value: suggestions))) |
There was a problem hiding this comment.
Always resolve the Future when self is deallocated.
The current early return path exits without calling promise, which can leave subscribers waiting indefinitely.
Proposed fix
return Future { [weak self] promise in
- guard let self else { return }
+ guard let self else {
+ promise(.success(SuggestionInfo(key: id, value: [MentionSuggestion]())))
+ return
+ }
nonisolated(unsafe) let unsafePromise = promise
Task { `@MainActor` in
let suggestions = await self.makeSuggestions(for: typingMention)
unsafePromise(.success(SuggestionInfo(key: id, value: suggestions)))
}
}π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return Future { [weak self] promise in | |
| guard let self else { return } | |
| nonisolated(unsafe) let unsafePromise = promise | |
| Task { @MainActor in | |
| let suggestions = await self.makeSuggestions(for: typingMention) | |
| unsafePromise(.success(SuggestionInfo(key: id, value: suggestions))) | |
| return Future { [weak self] promise in | |
| guard let self else { | |
| promise(.success(SuggestionInfo(key: id, value: [MentionSuggestion]()))) | |
| return | |
| } | |
| nonisolated(unsafe) let unsafePromise = promise | |
| Task { `@MainActor` in | |
| let suggestions = await self.makeSuggestions(for: typingMention) | |
| unsafePromise(.success(SuggestionInfo(key: id, value: suggestions))) |
π€ 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/Suggestions/Mentions/MentionsCommandHandler.swift`
around lines 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.
| .accessibilityHint( | ||
| Text( | ||
| L10n.Composer.Suggestions.User.accessibilityLabel( | ||
| "", | ||
| index + 1, | ||
| suggestions.count | ||
| ) |
There was a problem hiding this comment.
Per-row accessibility hint is incorrect for non-user mentions and omits the item name.
This path always uses the user-specific localization and passes an empty label argument, so VoiceOver output is misleading for @here, @channel, role, and group items.
As per coding guidelines: "Ensure components have accessibility labels, traits, and support for dynamic type sizing."
π€ 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/Suggestions/Mentions/MentionSuggestionsView.swift`
around lines 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.
Source: Coding guidelines
| "composer.suggestions.mentions.accessibility-announcement" = "User list, %d results"; | ||
| "composer.suggestions.mentions.channel.description" = "Notify everyone in this channel"; | ||
| "composer.suggestions.mentions.here.description" = "Notify online members"; | ||
| "composer.suggestions.mentions.role.description" = "Notify all %@ members"; | ||
| "composer.suggestions.mentions.group.members" = "%d members"; |
There was a problem hiding this comment.
Mentions announcement copy is outdated for the expanded suggestion types.
"User list, %d results" no longer matches behavior now that suggestions include here/channel/role/group items too.
As per coding guidelines: "Ensure components have accessibility labels, traits, and support for dynamic type sizing."
π€ 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/Resources/en.lproj/Localizable.strings` around
lines 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.
Source: Coding guidelines
SDK Size
|
StreamChatSwiftUI XCSize
Show 56 more objects
|
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)
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>
@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]) |
martinmitrevski
left a comment
There was a problem hiding this comment.
lgtm - just left few smaller comments
| public convenience init( | ||
| channelController: ChatChannelController, | ||
| userSearchController: ChatUserSearchController? = nil, | ||
| commandSymbol: String, | ||
| mentionAllAppUsers: Bool, | ||
| id: String = "mentions" | ||
| ) { | ||
| self.init( | ||
| channelController: channelController, | ||
| commandSymbol: commandSymbol, | ||
| provider: DefaultMentionSuggestionsProvider( | ||
| client: channelController.client, | ||
| mentionAllAppUsers: mentionAllAppUsers | ||
| ), | ||
| id: id | ||
| ) | ||
| } |
There was a problem hiding this comment.
this is a valid one
| } | ||
|
|
||
| /// A commands configuration that uses the ``EnhancedMentionSuggestionsProvider`` for mentions. | ||
| final class DemoCommandsConfig: CommandsConfig { |
There was a problem hiding this comment.
shouldn't we offer something like this in the SDK?
There was a problem hiding this comment.
That is a good point, we can already provide commands config that contains the enhanced mentions, will do that π
| case let userSuggestion as MentionSuggestion.User: | ||
| return userSuggestion.user.name ?? userSuggestion.user.id | ||
| case is MentionSuggestion.Here: | ||
| return "@here" |
There was a problem hiding this comment.
we should probably localize these
| mentionedRoles.remove(role) | ||
| } | ||
| mentionedGroups.removeAll { !text.contains("@\($0.name)") } | ||
| if mentionsHere, !text.contains("@here") { |
There was a problem hiding this comment.
probably also good if we don't compare directly to strings here, especially if we decide to localize
π 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
Release Notes
New Features
@here,@channel, roles, and groups in the composer, in addition to user mentions.Tests