From dfec55748bf62293228a8eda786696282c3221f3 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 12 Aug 2026 16:41:14 +0100 Subject: [PATCH 1/8] Discard stale mention suggestions when clearing the query Cancel in-flight suggestion requests and ignore superseded results so deleting an @mention query does not briefly re-show the suggestions list. --- .../MessageComposerViewModel.swift | 43 ++++++++++++----- .../MessageComposerViewModel_Tests.swift | 48 +++++++++++++++++++ 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift index 373319695..eaa3d1cb7 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift @@ -36,12 +36,15 @@ import SwiftUI channelController.sendKeystrokeEvent() } } else { - if composerCommand?.displayInfo?.isInstant == false { + // Mentions use `displayInfo == nil`, so clear any non-instant command + // (not only commands with `isInstant == false`). + if composerCommand?.displayInfo?.isInstant != true { withAnimation(.easeInOut(duration: 0.2)) { composerCommand = nil } } selectedRangeLocation = 0 + suggestionsCancellable?.cancel() withAnimation(.easeInOut(duration: 0.2)) { suggestions = [String: Any]() } @@ -211,6 +214,8 @@ import SwiftUI } private var cancellables = Set() + /// Tracks the in-flight suggestions request so stale results can be discarded. + private var suggestionsCancellable: AnyCancellable? public lazy var commandsHandler = utils .commandsConfig .makeCommandsHandler( @@ -1040,17 +1045,33 @@ import SwiftUI } private func showTypingSuggestions() { - if let composerCommand { - commandsHandler.showSuggestions(for: composerCommand) - .sink { _ in - log.debug("Finished showing suggestions") - } receiveValue: { [weak self] suggestionInfo in - withAnimation { - self?.suggestions[suggestionInfo.key] = suggestionInfo.value - } - } - .store(in: &cancellables) + suggestionsCancellable?.cancel() + + guard let composerCommand else { + withAnimation(.easeInOut(duration: 0.2)) { + suggestions = [String: Any]() + } + return } + + // Capture the query that started this request so a slower, superseded + // search cannot overwrite newer (or cleared) suggestions while deleting. + let expectedCommandId = composerCommand.id + let expectedTypingText = composerCommand.typingSuggestion.text + + suggestionsCancellable = commandsHandler.showSuggestions(for: composerCommand) + .sink { _ in + log.debug("Finished showing suggestions") + } receiveValue: { [weak self] suggestionInfo in + guard let self else { return } + guard self.composerCommand?.id == expectedCommandId, + self.composerCommand?.typingSuggestion.text == expectedTypingText else { + return + } + withAnimation { + self.suggestions[suggestionInfo.key] = suggestionInfo.value + } + } } private func listenToCooldownUpdates() { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift index f3f850845..4bb70111e 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift @@ -686,6 +686,54 @@ import XCTest XCTAssert(viewModel.mentionedUsers.isEmpty) } + func test_messageComposerVM_clearingTextClearsMentionSuggestionsAndCommand() { + let viewModel = makeComposerViewModel() + viewModel.suggestions = ["mentions": [ChatUser.mock(id: "ios", name: "iOS")]] + viewModel.composerCommand = ComposerCommand( + id: "mentions", + typingSuggestion: TypingSuggestion(text: "i", locationRange: NSRange(location: 1, length: 1)), + displayInfo: nil + ) + + viewModel.text = "" + + XCTAssertNil(viewModel.composerCommand) + XCTAssertTrue(viewModel.suggestions.isEmpty) + } + + func test_messageComposerVM_endingMentionClearsSuggestions() { + let viewModel = makeComposerViewModel() + viewModel.selectedRangeLocation = 8 + viewModel.text = "hello @i" + XCTAssertEqual(viewModel.composerCommand?.id, "mentions") + + viewModel.selectedRangeLocation = 6 + viewModel.text = "hello " + + XCTAssertNil(viewModel.composerCommand) + XCTAssertTrue(viewModel.suggestions.isEmpty) + } + + func test_messageComposerVM_deletingMentionQuery_doesNotShowStaleSuggestionsWhenEmpty() async { + let viewModel = makeComposerViewModel() + viewModel.selectedRangeLocation = 4 + viewModel.text = "@iOS" + viewModel.selectedRangeLocation = 3 + viewModel.text = "@iO" + viewModel.selectedRangeLocation = 2 + viewModel.text = "@i" + viewModel.selectedRangeLocation = 1 + viewModel.text = "@" + viewModel.selectedRangeLocation = 0 + viewModel.text = "" + + // Allow any in-flight / debounced suggestion request to finish. + try? await Task.sleep(nanoseconds: 800_000_000) + + XCTAssertNil(viewModel.composerCommand) + XCTAssertTrue(viewModel.suggestions.isEmpty) + } + func test_checkForMentionedUsers_withUserSuggestion() { // Given let viewModel = makeComposerViewModel() From 67f173a1922c5cff9adc8818a6015052d6512183 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 12 Aug 2026 16:49:16 +0100 Subject: [PATCH 2/8] Clear mention suggestion search state when the query ends Forward clearSuggestions through command handlers so MentionsCommandHandler cancels in-flight work and clears provider results when the composer leaves mention mode. --- .../MessageComposerViewModel.swift | 18 ++++---- .../Suggestions/CommandsHandler.swift | 16 ++++++++ .../Mentions/MentionsCommandHandler.swift | 11 ++++- .../MentionsCommandHandler_Tests.swift | 41 ++++++++++++++++++- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift index eaa3d1cb7..290e85cdb 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift @@ -44,10 +44,7 @@ import SwiftUI } } selectedRangeLocation = 0 - suggestionsCancellable?.cancel() - withAnimation(.easeInOut(duration: 0.2)) { - suggestions = [String: Any]() - } + clearComposerSuggestions() clearMentions() if shouldDeleteDraftMessage(oldValue: oldValue) { @@ -122,6 +119,7 @@ import SwiftUI } if oldValue != nil && composerCommand == nil { pickerTypeState = .expanded(.none) + clearComposerSuggestions() } } } @@ -1048,9 +1046,7 @@ import SwiftUI suggestionsCancellable?.cancel() guard let composerCommand else { - withAnimation(.easeInOut(duration: 0.2)) { - suggestions = [String: Any]() - } + clearComposerSuggestions() return } @@ -1073,6 +1069,14 @@ import SwiftUI } } } + + private func clearComposerSuggestions() { + suggestionsCancellable?.cancel() + commandsHandler.clearSuggestions() + withAnimation(.easeInOut(duration: 0.2)) { + suggestions = [String: Any]() + } + } private func listenToCooldownUpdates() { channelController.channelChangePublisher.sink { [weak self] _ in diff --git a/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/CommandsHandler.swift b/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/CommandsHandler.swift index c2784e524..566970b73 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/CommandsHandler.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/CommandsHandler.swift @@ -65,6 +65,12 @@ import SwiftUI composerCommand: ComposerCommand, completion: @escaping @MainActor (Error?) -> Void ) + + /// Cancels any pending suggestion work and clears cached search results. + /// + /// Called when the active command ends so a slower, superseded search cannot + /// resurface stale suggestions. + func clearSuggestions() } /// Default implementations. @@ -83,6 +89,10 @@ extension CommandHandler { public func canBeExecuted(composerCommand: ComposerCommand) -> Bool { !composerCommand.typingSuggestion.text.isEmpty } + + public func clearSuggestions() { + // optional method. + } } /// Model for the composer's commands. @@ -255,6 +265,12 @@ public class CommandsHandler: CommandHandler { return StreamChatError.noSuggestionsAvailable.asFailedPromise() } + public func clearSuggestions() { + for command in commands { + command.clearSuggestions() + } + } + public func handleCommand( for text: Binding, selectedRangeLocation: Binding, diff --git a/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionsCommandHandler.swift b/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionsCommandHandler.swift index 1b403c556..6f753f490 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionsCommandHandler.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/Suggestions/Mentions/MentionsCommandHandler.swift @@ -15,6 +15,7 @@ public final class MentionsCommandHandler: CommandHandler { private let channelController: ChatChannelController private let provider: MentionSuggestionsProvider + private var suggestionsTask: Task? /// Creates a new mentions command handler. /// @@ -123,6 +124,12 @@ public final class MentionsCommandHandler: CommandHandler { ) } + public func clearSuggestions() { + suggestionsTask?.cancel() + suggestionsTask = nil + Task { await provider.clearResults() } + } + func mentionText(for suggestion: MentionSuggestion) -> String { switch suggestion.kind { case let userSuggestion as MentionSuggestion.User: @@ -147,14 +154,16 @@ public final class MentionsCommandHandler: CommandHandler { mentionRange: NSRange ) -> Future { let id = id + suggestionsTask?.cancel() 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 + self.suggestionsTask = Task { @MainActor in let suggestions = await self.makeSuggestions(for: typingMention) + guard !Task.isCancelled else { return } unsafePromise(.success(SuggestionInfo(key: id, value: suggestions))) } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionsCommandHandler_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionsCommandHandler_Tests.swift index 64bb8d88a..e69b7a891 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionsCommandHandler_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/MentionsCommandHandler_Tests.swift @@ -289,6 +289,30 @@ import XCTest XCTAssertTrue(provider.receivedRequests.isEmpty) } + func test_clearSuggestions_cancelsInFlightAndClearsProvider() async { + // Given + let provider = MockMentionSuggestionsProvider(suggestions: [.here], delayNanoseconds: 500_000_000) + let handler = makeHandler(provider: provider) + let expectation = expectation(description: "suggestions") + expectation.isInverted = true + + let cancellable = handler.showSuggestions(for: mentionsCommand(text: "mar")).sink { _ in + } receiveValue: { _ in + expectation.fulfill() + } + + // When + handler.clearSuggestions() + + // Then + await fulfillment(of: [expectation], timeout: 0.2) + cancellable.cancel() + + // Allow the async provider clear to run. + try? await Task.sleep(nanoseconds: 50_000_000) + XCTAssertEqual(provider.clearResultsCallCount, 1) + } + // MARK: - private private func makeHandler(provider: MentionSuggestionsProvider? = nil) -> MentionsCommandHandler { @@ -328,18 +352,33 @@ private final class Box { private final class MockMentionSuggestionsProvider: MentionSuggestionsProvider, @unchecked Sendable { let suggestions: [MentionSuggestion] let error: Error? + let delayNanoseconds: UInt64 private(set) var receivedRequests: [MentionSuggestionsRequest] = [] + private(set) var clearResultsCallCount = 0 - init(suggestions: [MentionSuggestion] = [], error: Error? = nil) { + init( + suggestions: [MentionSuggestion] = [], + error: Error? = nil, + delayNanoseconds: UInt64 = 0 + ) { self.suggestions = suggestions self.error = error + self.delayNanoseconds = delayNanoseconds } func mentionSuggestions(for request: MentionSuggestionsRequest) async throws -> [MentionSuggestion] { receivedRequests.append(request) + if delayNanoseconds > 0 { + try await Task.sleep(nanoseconds: delayNanoseconds) + } + try Task.checkCancellation() if let error { throw error } return suggestions } + + func clearResults() async { + clearResultsCallCount += 1 + } } From 63b1c8c7e0dd7f44ac70e6d1caf9f71d1b31a1e6 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 12 Aug 2026 16:53:56 +0100 Subject: [PATCH 3/8] Point StreamChat dependency at mention suggestions branch Use the LLC fix/mention-suggestions-large-channels branch so SwiftUI picks up clearResults and MemberSearch while that PR is open. --- Package.swift | 2 +- StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index db450e42f..b50bc37a9 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/GetStream/stream-chat-swift.git", revision: "412e8a01c00481c60ae86e8184620023f136e0b7") + .package(url: "https://github.com/GetStream/stream-chat-swift.git", branch: "fix/mention-suggestions-large-channels") ], targets: [ .target( diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index fc8f16cbf..bc87324fa 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -1543,8 +1543,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - kind = revision; - revision = 412e8a01c00481c60ae86e8184620023f136e0b7; + kind = branch; + branch = "fix/mention-suggestions-large-channels"; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { From 7b68035071abb7640264d53aeccc3f3a7fe9f26a Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 12 Aug 2026 17:29:14 +0100 Subject: [PATCH 4/8] Avoid double-clearing composer suggestions and harden mention tests Clear suggestions once when emptying text, and replace sleep-based stale-mention coverage with expectation-driven delayed provider requests. --- .../MessageComposerViewModel.swift | 9 ++- .../MessageComposerViewModel_Tests.swift | 78 ++++++++++++++++--- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift index 290e85cdb..db469d71d 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift @@ -38,13 +38,18 @@ import SwiftUI } else { // Mentions use `displayInfo == nil`, so clear any non-instant command // (not only commands with `isInstant == false`). - if composerCommand?.displayInfo?.isInstant != true { + // When a command is cleared here, `composerCommand` didSet clears suggestions; + // otherwise clear them explicitly (instant command kept, or no command). + let clearingCommand = composerCommand != nil + && composerCommand?.displayInfo?.isInstant != true + if clearingCommand { withAnimation(.easeInOut(duration: 0.2)) { composerCommand = nil } + } else { + clearComposerSuggestions() } selectedRangeLocation = 0 - clearComposerSuggestions() clearMentions() if shouldDeleteDraftMessage(oldValue: oldValue) { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift index 4bb70111e..278a58b89 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift @@ -715,20 +715,29 @@ import XCTest } func test_messageComposerVM_deletingMentionQuery_doesNotShowStaleSuggestionsWhenEmpty() async { - let viewModel = makeComposerViewModel() + let requestStarted = expectation(description: "delayed mention request started") + let requestFinished = expectation(description: "delayed mention request finished") + let provider = DelayedMentionSuggestionsProvider( + suggestions: [.user(.mock(id: "ios", name: "iOS"))], + delayNanoseconds: 100_000_000, + onStarted: { requestStarted.fulfill() }, + onFinished: { requestFinished.fulfill() } + ) + let viewModel = MessageComposerViewModel( + channelController: makeChannelController(), + messageController: nil + ) + viewModel.utils = Utils( + commandsConfig: MentionsProviderCommandsConfig(provider: provider) + ) + viewModel.selectedRangeLocation = 4 viewModel.text = "@iOS" - viewModel.selectedRangeLocation = 3 - viewModel.text = "@iO" - viewModel.selectedRangeLocation = 2 - viewModel.text = "@i" - viewModel.selectedRangeLocation = 1 - viewModel.text = "@" + await fulfillment(of: [requestStarted], timeout: defaultTimeout) + viewModel.selectedRangeLocation = 0 viewModel.text = "" - - // Allow any in-flight / debounced suggestion request to finish. - try? await Task.sleep(nanoseconds: 800_000_000) + await fulfillment(of: [requestFinished], timeout: defaultTimeout) XCTAssertNil(viewModel.composerCommand) XCTAssertTrue(viewModel.suggestions.isEmpty) @@ -2839,3 +2848,52 @@ enum MessageComposerTestUtils { ) } } + +private final class MentionsProviderCommandsConfig: CommandsConfig { + let mentionsSymbol = "@" + let instantCommandsSymbol = "/" + private let provider: MentionSuggestionsProvider + + init(provider: MentionSuggestionsProvider) { + self.provider = provider + } + + func makeCommandsHandler(with channelController: ChatChannelController) -> CommandsHandler { + let mentionsCommandHandler = MentionsCommandHandler( + channelController: channelController, + commandSymbol: mentionsSymbol, + provider: provider + ) + return DefaultCommandsConfig.makeCommandsHandler( + mentionsCommandHandler: mentionsCommandHandler, + channelController: channelController + ) + } +} + +private final class DelayedMentionSuggestionsProvider: MentionSuggestionsProvider, @unchecked Sendable { + private let suggestions: [MentionSuggestion] + private let delayNanoseconds: UInt64 + private let onStarted: @Sendable () -> Void + private let onFinished: @Sendable () -> Void + + init( + suggestions: [MentionSuggestion], + delayNanoseconds: UInt64, + onStarted: @escaping @Sendable () -> Void, + onFinished: @escaping @Sendable () -> Void + ) { + self.suggestions = suggestions + self.delayNanoseconds = delayNanoseconds + self.onStarted = onStarted + self.onFinished = onFinished + } + + func mentionSuggestions(for request: MentionSuggestionsRequest) async throws -> [MentionSuggestion] { + onStarted() + defer { onFinished() } + try await Task.sleep(nanoseconds: delayNanoseconds) + try Task.checkCancellation() + return suggestions + } +} From 4ca185a6b9b9f567d105e82f39e7fbc0aecdc1ea Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Thu, 13 Aug 2026 18:34:34 +0100 Subject: [PATCH 5/8] Pin StreamChat to merged mention suggestions commit Use develop revision 8263bddc2 now that #4213 is merged. --- Package.swift | 2 +- StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index b50bc37a9..596dac060 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/GetStream/stream-chat-swift.git", branch: "fix/mention-suggestions-large-channels") + .package(url: "https://github.com/GetStream/stream-chat-swift.git", revision: "8263bddc2f9395fb1c4a275c86db711ceda954ed") ], targets: [ .target( diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index bc87324fa..34a1ff0eb 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -1543,8 +1543,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - kind = branch; - branch = "fix/mention-suggestions-large-channels"; + kind = revision; + revision = 8263bddc2f9395fb1c4a275c86db711ceda954ed; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { From 64c9a7713ee36004d3097ccd9534adcffeec486f Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Thu, 13 Aug 2026 18:35:35 +0100 Subject: [PATCH 6/8] Address PR review nits in suggestion clearing Drop the debug log from the suggestions sink and use removeAll when clearing suggestions. --- .../ChatComposer/MessageComposerViewModel.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift index db469d71d..36b8411bd 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift @@ -1061,9 +1061,7 @@ import SwiftUI let expectedTypingText = composerCommand.typingSuggestion.text suggestionsCancellable = commandsHandler.showSuggestions(for: composerCommand) - .sink { _ in - log.debug("Finished showing suggestions") - } receiveValue: { [weak self] suggestionInfo in + .sink { [weak self] suggestionInfo in guard let self else { return } guard self.composerCommand?.id == expectedCommandId, self.composerCommand?.typingSuggestion.text == expectedTypingText else { @@ -1079,7 +1077,7 @@ import SwiftUI suggestionsCancellable?.cancel() commandsHandler.clearSuggestions() withAnimation(.easeInOut(duration: 0.2)) { - suggestions = [String: Any]() + suggestions.removeAll() } } From 655a8c62a82b8923d7ed711b45fa166a6e8cca7d Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Thu, 13 Aug 2026 21:31:33 +0100 Subject: [PATCH 7/8] Fix compilation error in suggestion sink closure The Future publisher's sink requires both receiveCompletion and receiveValue closures; the previous commit dropped receiveCompletion, causing a type mismatch. --- .../ChatComposer/MessageComposerViewModel.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift index 36b8411bd..2695c4db9 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift @@ -1061,7 +1061,8 @@ import SwiftUI let expectedTypingText = composerCommand.typingSuggestion.text suggestionsCancellable = commandsHandler.showSuggestions(for: composerCommand) - .sink { [weak self] suggestionInfo in + .sink(receiveCompletion: { _ in }, + receiveValue: { [weak self] suggestionInfo in guard let self else { return } guard self.composerCommand?.id == expectedCommandId, self.composerCommand?.typingSuggestion.text == expectedTypingText else { @@ -1070,7 +1071,7 @@ import SwiftUI withAnimation { self.suggestions[suggestionInfo.key] = suggestionInfo.value } - } + }) } private func clearComposerSuggestions() { From f00f32f540c54e95dd631501b746921f3aca3efc Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Thu, 13 Aug 2026 22:48:19 +0100 Subject: [PATCH 8/8] Fix SwiftFormat indentation in suggestion sink closure --- .../MessageComposerViewModel.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift index 2695c4db9..20ac84bf0 100644 --- a/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatComposer/MessageComposerViewModel.swift @@ -1061,17 +1061,19 @@ import SwiftUI let expectedTypingText = composerCommand.typingSuggestion.text suggestionsCancellable = commandsHandler.showSuggestions(for: composerCommand) - .sink(receiveCompletion: { _ in }, - receiveValue: { [weak self] suggestionInfo in - guard let self else { return } - guard self.composerCommand?.id == expectedCommandId, - self.composerCommand?.typingSuggestion.text == expectedTypingText else { - return - } - withAnimation { - self.suggestions[suggestionInfo.key] = suggestionInfo.value + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] suggestionInfo in + guard let self else { return } + guard self.composerCommand?.id == expectedCommandId, + self.composerCommand?.typingSuggestion.text == expectedTypingText else { + return + } + withAnimation { + self.suggestions[suggestionInfo.key] = suggestionInfo.value + } } - }) + ) } private func clearComposerSuggestions() {