From 8a1bc9c1cb50e5f4740c083f6125037b74c1ed4a Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 22 Oct 2025 18:41:01 +0000 Subject: [PATCH 01/11] Update release version to snapshot --- .../StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index a5c3c79c..05c0a678 100644 --- a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift +++ b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift @@ -7,5 +7,5 @@ import Foundation enum SystemEnvironment { /// A Stream Chat version. - public static let version: String = "4.91.0" + public static let version: String = "4.92.0-SNAPSHOT" } From 308a1d3bdebb1759d7069d8e9e33d3dcf0c2f861 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 23 Oct 2025 13:40:50 +0100 Subject: [PATCH 02/11] [CI] Resolve mock server updates (#1029) --- StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift | 2 +- StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift | 2 +- StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift | 2 +- .../Tests/PushNotification_Tests.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift b/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift index 3ab1f0b8..cbd910de 100644 --- a/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift +++ b/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift @@ -656,7 +656,7 @@ extension UserRobot { @discardableResult func assertMentionWasApplied(file: StaticString = #filePath, line: UInt = #line) -> Self { - let expectedText = "@\(UserDetails.hanSoloName)" + let expectedText = "@\(UserDetails.countDookuName)" let actualText = MessageListPage.Composer.textView.waitForText(expectedText).text XCTAssertEqual(expectedText, actualText, file: file, line: line) return self diff --git a/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift b/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift index f03af913..f0afd185 100644 --- a/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift +++ b/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift @@ -423,7 +423,7 @@ extension UserRobot { @discardableResult func mentionParticipant(manually: Bool = false) -> Self { - let text = "@\(UserDetails.hanSoloId)" + let text = "@\(UserDetails.countDookuId)" if manually { typeText(text) } else { diff --git a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift index 8ee85d95..7cdce68c 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift @@ -103,7 +103,7 @@ final class MessageList_Tests: StreamTestCase { linkToScenario(withId: 254) let message = "message" - let author = "Han Solo" + let author = "Count Dooku" GIVEN("user opens the channel") { userRobot.login().openChannel() diff --git a/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift index 65ca6f65..243c5026 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift @@ -6,7 +6,7 @@ import XCTest // Requires running a standalone Sinatra server final class PushNotification_Tests: StreamTestCase { - let sender = "Han Solo" + let sender = "Count Dooku" let message = "How are you? 🙂" override func setUpWithError() throws { From 5f7faa6d905cd3ebc152da2a43f79aa6bd4909cd Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Thu, 23 Oct 2025 15:47:43 +0100 Subject: [PATCH 03/11] Fix composer deleting newly entered text after deleting draft text (#1030) * Fix new input text in the composer deleted after quickly deleting previous text from draft * Update CHANGELOG.md --- CHANGELOG.md | 3 +- .../Composer/MessageComposerViewModel.swift | 8 --- .../MessageComposerViewModel_Tests.swift | 68 ------------------- 3 files changed, 2 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5512f5ba..ef1cf933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming -### 🔄 Changed +### 🐞 Fixed +- Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030) # [4.91.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.91.0) _October 22, 2025_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift index 48e1b105..545b5a57 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift @@ -908,14 +908,6 @@ extension MessageComposerViewModel: EventsControllerDelegate { fillDraftMessage() } } - - if let event = event as? DraftDeletedEvent { - let isFromSameThread = messageController?.messageId == event.threadId - let isFromSameChannel = channelController.cid == event.cid && messageController == nil - if isFromSameThread || isFromSameChannel { - clearInputData() - } - } } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift index 17623396..6c9dd520 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift @@ -1109,74 +1109,6 @@ class MessageComposerViewModel_Tests: StreamChatTestCase { XCTAssertEqual(viewModel.text, "Draft") } - func test_messageComposerVM_draftMessageDeletedEvent() throws { - // Given - let channelController = makeChannelController() - channelController.channel_mock = .mock(cid: .unique, draftMessage: .mock(text: "Draft")) - let viewModel = makeComposerDraftsViewModel( - channelController: channelController, - messageController: nil - ) - - // When - channelController.channel_mock = .mock(cid: .unique, draftMessage: nil) - let cid = try XCTUnwrap(channelController.cid) - let event = DraftDeletedEvent(cid: cid, threadId: nil, createdAt: .unique) - viewModel.eventsController(viewModel.eventsController, didReceiveEvent: event) - - // Then - XCTAssertEqual(viewModel.text, "") - } - - func test_messageComposerVM_draftReplyDeletedEvent() throws { - // Given - let channelController = makeChannelController() - let messageController = ChatMessageControllerSUI_Mock.mock( - chatClient: chatClient, - cid: channelController.cid!, - messageId: .unique - ) - messageController.message_mock = .mock(draftReply: .mock(text: "Draft")) - let viewModel = makeComposerDraftsViewModel( - channelController: channelController, - messageController: messageController - ) - - // When - messageController.message_mock = .mock(draftReply: nil) - let cid = try XCTUnwrap(channelController.cid) - let event = DraftDeletedEvent(cid: cid, threadId: messageController.messageId, createdAt: .unique) - viewModel.eventsController(viewModel.eventsController, didReceiveEvent: event) - - // Then - XCTAssertEqual(viewModel.text, "") - } - - func test_messageComposerVM_draftReplyDeletedEventFromOtherThread_shouldNotUpdate() throws { - // Given - let channelController = makeChannelController() - let messageController = ChatMessageControllerSUI_Mock.mock( - chatClient: chatClient, - cid: channelController.cid!, - messageId: .unique - ) - messageController.message_mock = .mock(draftReply: .mock(text: "Draft")) - let viewModel = makeComposerDraftsViewModel( - channelController: channelController, - messageController: messageController - ) - viewModel.fillDraftMessage() - - // When - messageController.message_mock = .mock(draftReply: nil) - let cid = try XCTUnwrap(channelController.cid) - let event = DraftDeletedEvent(cid: cid, threadId: .unique, createdAt: .unique) - viewModel.eventsController(viewModel.eventsController, didReceiveEvent: event) - - // Then - XCTAssertEqual(viewModel.text, "Draft") - } - func test_messageComposerVM_whenLastAssetRemoved_shouldDeleteDraft() { // Given let channelController = makeChannelController() From 02bd443860d6e601b7e2a3dc39c9ec754aa67b03 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 23 Oct 2025 18:38:39 +0100 Subject: [PATCH 04/11] Add docs archive generation guide to README.md (#1031) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 8f326406..e4478e92 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,17 @@ The SwiftUI SDK offers three types of components: - Stateful components - Offer more customization options and possibility to inject custom views. Also fairly simple to integrate, if the extension points are suitable for your chat use-case. These components come with view models. - Stateless components - These are the building blocks for the other two types of components. In order to use them, you would have to provide the state and data. Using these components only make sense if you want to implement completely custom chat experience. +## Documentation Generation + +To generate the documentation for SwiftUI StreamChat SDK, run the following command: + +```bash +xcodebuild docbuild -skipMacroValidation -skipPackagePluginValidation -derivedDataPath .derivedData -scheme StreamChatSwiftUI -destination generic/platform=iOS | xcpretty +open .derivedData/Build/Products/Debug-iphoneos/StreamChatSwiftUI.doccarchive +``` + +This will build the documentation archive and automatically open it in Xcode. + ## Free for Makers Stream is free for most side and hobby projects. You can use Stream Chat for free if you have less than five team members and no more than $10,000 in monthly revenue. From 9eae155367e49ecfb3e84328ca223c9a9312e2ab Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 31 Oct 2025 12:08:08 +0000 Subject: [PATCH 05/11] Add message highlighting on jumping to a quoted message (#1032) --- CHANGELOG.md | 3 + .../ChatChannel/ChatChannelView.swift | 1 + .../ChatChannel/ChatChannelViewModel.swift | 38 +++++- .../MessageList/MessageContainerView.swift | 25 +++- .../MessageList/MessageRepliesView.swift | 12 +- Sources/StreamChatSwiftUI/ColorPalette.swift | 2 + .../DefaultViewFactory.swift | 3 +- .../ChatChannelViewModel_Tests.swift | 118 +++++++++++++++++- .../MessageContainerView_Tests.swift | 45 ++++++- ...messageContainerHighlighted_snapshot.1.png | Bin 0 -> 28863 bytes ...sageContainerNotHighlighted_snapshot.1.png | Bin 0 -> 32255 bytes 11 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerNotHighlighted_snapshot.1.png diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1cf933..d2342e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### ✅ Added +- Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1030) + ### 🐞 Fixed - Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index 47ff4277..17cb5476 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -73,6 +73,7 @@ public struct ChatChannelView: View, KeyboardReadable { }, onJumpToMessage: viewModel.jumpToMessage(messageId:) ) + .environment(\.highlightedMessageId, viewModel.highlightedMessageId) .dismissKeyboardOnTap(enabled: true) { hideComposerCommandsAndAttachmentsPicker() } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index 8ed4ee79..f37c6722 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -56,6 +56,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { public var messageController: ChatMessageController? @Published public var scrolledId: String? + @Published public var highlightedMessageId: String? @Published public var listId = UUID().uuidString @Published public var showScrollToLatestButton = false @@ -172,6 +173,18 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { self?.messageCachingUtils.jumpToReplyId = scrollToMessage.messageId } else if messageController != nil, let jumpToReplyId = self?.messageCachingUtils.jumpToReplyId { self?.scrolledId = jumpToReplyId + // Trigger highlight when jumping to reply in thread + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.highlightedMessageId = jumpToReplyId + } + // Clear scroll ID after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.scrolledId = nil + } + // Clear highlight after animation completes + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in + self?.highlightedMessageId = nil + } self?.messageCachingUtils.jumpToReplyId = nil } else if messageController == nil { self?.scrolledId = scrollToMessage?.messageId @@ -232,6 +245,12 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if let message = notification.userInfo?[MessageRepliesConstants.selectedMessage] as? ChatMessage { threadMessage = message threadMessageShown = true + + // Only set jumpToReplyId if there's a specific reply message to highlight + // (for showReplyInChannel messages). The parent message should never be highlighted. + if let replyMessage = notification.userInfo?[MessageRepliesConstants.threadReplyMessage] as? ChatMessage { + messageCachingUtils.jumpToReplyId = replyMessage.messageId + } } } @@ -297,9 +316,18 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if scrolledId == nil { scrolledId = messageId } + // Trigger highlight after a short delay to allow scroll animation to start + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.highlightedMessageId = messageId + } + // Clear scroll ID after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.scrolledId = nil } + // Clear highlight after animation completes (0.6s delay from StreamChatUI implementation) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in + self?.highlightedMessageId = nil + } return true } else { let message = channelController.dataStore.message(id: baseId) @@ -325,9 +353,17 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if toJumpId == baseId, let message = self?.channelController.dataStore.message(id: toJumpId) { toJumpId = message.messageId } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.scrolledId = toJumpId self?.loadingMessagesAround = false + // Trigger highlight after scroll starts + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self?.highlightedMessageId = toJumpId + } + // Clear highlight after animation completes + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { + self?.highlightedMessageId = nil + } } } return false diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift index fefb9088..656faf46 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift @@ -9,7 +9,8 @@ import SwiftUI public struct MessageContainerView: View { @StateObject var messageViewModel: MessageViewModel @Environment(\.channelTranslationLanguage) var translationLanguage - + @Environment(\.highlightedMessageId) var highlightedMessageId + @Injected(\.fonts) private var fonts @Injected(\.colors) private var colors @Injected(\.images) private var images @@ -284,7 +285,15 @@ public struct MessageContainerView: View { .padding(.horizontal, messageListConfig.messagePaddings.horizontal) .padding(.bottom, showsAllInfo || messageViewModel.isPinned ? paddingValue : groupMessageInterItemSpacing) .padding(.top, isLast ? paddingValue : 0) - .background(messageViewModel.isPinned ? Color(colors.pinnedBackground) : nil) + .background( + Group { + if let highlightedMessageId = highlightedMessageId, highlightedMessageId == message.messageId { + Color(colors.messageCellHighlightBackground) + } else if messageViewModel.isPinned { + Color(colors.pinnedBackground) + } + } + ) .padding(.bottom, messageViewModel.isPinned ? paddingValue / 2 : 0) .transition( message.isSentByCurrentUser ? @@ -398,6 +407,18 @@ public struct MessageContainerView: View { } } +// Environment plumbing colocated to avoid adding new files to the package list. +private struct HighlightedMessageIdKey: EnvironmentKey { + static let defaultValue: String? = nil +} + +extension EnvironmentValues { + var highlightedMessageId: String? { + get { self[HighlightedMessageIdKey.self] } + set { self[HighlightedMessageIdKey.self] = newValue } + } +} + struct SendFailureIndicator: View { @Injected(\.colors) private var colors @Injected(\.images) private var images diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift index df3af5cf..438fd08e 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift @@ -8,6 +8,7 @@ import SwiftUI enum MessageRepliesConstants { static let selectedMessageThread = "selectedMessageThread" static let selectedMessage = "selectedMessage" + static let threadReplyMessage = "threadReplyMessage" } /// View shown below a message, when there are replies to it. @@ -21,6 +22,7 @@ public struct MessageRepliesView: View { var replyCount: Int var isRightAligned: Bool var showReplyCount: Bool + var threadReplyMessage: ChatMessage? // The actual reply message (for showReplyInChannel messages) public init( factory: Factory, @@ -28,7 +30,8 @@ public struct MessageRepliesView: View { message: ChatMessage, replyCount: Int, showReplyCount: Bool = true, - isRightAligned: Bool? = nil + isRightAligned: Bool? = nil, + threadReplyMessage: ChatMessage? = nil ) { self.factory = factory self.channel = channel @@ -36,6 +39,7 @@ public struct MessageRepliesView: View { self.replyCount = replyCount self.isRightAligned = isRightAligned ?? message.isRightAligned self.showReplyCount = showReplyCount + self.threadReplyMessage = threadReplyMessage } public var body: some View { @@ -44,10 +48,14 @@ public struct MessageRepliesView: View { resignFirstResponder() // NOTE: this is used to avoid breaking changes. // Will be updated in a major release. + var userInfo: [String: Any] = [MessageRepliesConstants.selectedMessage: message] + if let threadReplyMessage = threadReplyMessage { + userInfo[MessageRepliesConstants.threadReplyMessage] = threadReplyMessage + } NotificationCenter.default.post( name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread), object: nil, - userInfo: [MessageRepliesConstants.selectedMessage: message] + userInfo: userInfo ) } label: { HStack { diff --git a/Sources/StreamChatSwiftUI/ColorPalette.swift b/Sources/StreamChatSwiftUI/ColorPalette.swift index 5006ef98..e38841bb 100644 --- a/Sources/StreamChatSwiftUI/ColorPalette.swift +++ b/Sources/StreamChatSwiftUI/ColorPalette.swift @@ -60,6 +60,7 @@ public struct ColorPalette { public var highlightedAccentBackground: UIColor = .streamAccentBlue public var highlightedAccentBackground1: UIColor = .streamBlueAlice public var pinnedBackground: UIColor = .streamHighlight + public var messageCellHighlightBackground: UIColor = .streamYellowBackground // MARK: - Borders and shadows @@ -167,6 +168,7 @@ private extension UIColor { static let streamGrayDisabledText = mode(0x72767e, 0x72767e) static let streamInnerBorder = mode(0xdbdde1, 0x272a30) static let streamHighlight = mode(0xfbf4dd, 0x333024) + static let streamYellowBackground = mode(0xfff2a1, 0x4a3d00) static let streamDisabled = mode(0xb4b7bb, 0x4c525c) // Currently we are not using the correct shadow color from figma's color palette. This is to avoid diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index 3d881999..19e18fd3 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -608,7 +608,8 @@ extension ViewFactory { message: parentMessage, replyCount: replyCount, showReplyCount: false, - isRightAligned: message.isRightAligned + isRightAligned: message.isRightAligned, + threadReplyMessage: message // Pass the actual reply message (shown in channel) ) } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift index 34495e0d..8b267172 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift @@ -534,14 +534,126 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { let message2 = ChatMessage.mock() let channelController = makeChannelController(messages: [message1, message2]) let viewModel = ChatChannelViewModel(channelController: channelController) - + // When let shouldJump = viewModel.jumpToMessage(messageId: .unknownMessageId) - + // Then XCTAssert(shouldJump == false) } - + + func test_chatChannelVM_jumpToMessage_setsHighlightedMessageId() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + let testExpectation = XCTestExpectation(description: "Highlight should be set") + testExpectation.assertForOverFulfill = false + + // When + let shouldJump = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + XCTAssert(shouldJump == true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertEqual(viewModel.highlightedMessageId, message2.messageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: 1.0) + } + + func test_chatChannelVM_jumpToMessage_clearsHighlightedMessageId() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + let testExpectation = XCTestExpectation(description: "Highlight should be cleared") + + // When + _ = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + XCTAssertNil(viewModel.highlightedMessageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: 1.5) + } + + func test_chatChannelVM_jumpToMessage_setsScrolledId() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + + // When + _ = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + XCTAssertEqual(viewModel.scrolledId, message2.messageId) + } + + func test_chatChannelVM_selectedMessageThread_opensThread() { + // Given + let channelController = makeChannelController() + let viewModel = ChatChannelViewModel(channelController: channelController) + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test message", + author: .mock(id: .unique) + ) + + // When + NotificationCenter.default.post( + name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread), + object: nil, + userInfo: [MessageRepliesConstants.selectedMessage: message] + ) + + // Then + XCTAssertEqual(viewModel.threadMessage, message) + XCTAssertTrue(viewModel.threadMessageShown) + } + + func test_chatChannelVM_selectedMessageThread_withThreadReplyMessage_opensThread() { + // Given + let channelController = makeChannelController() + let viewModel = ChatChannelViewModel(channelController: channelController) + let parentMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Parent message", + author: .mock(id: .unique) + ) + let replyMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Reply message", + author: .mock(id: .unique), + parentMessageId: parentMessage.id + ) + + // When + NotificationCenter.default.post( + name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread), + object: nil, + userInfo: [ + MessageRepliesConstants.selectedMessage: parentMessage, + MessageRepliesConstants.threadReplyMessage: replyMessage + ] + ) + + // Then + XCTAssertEqual(viewModel.threadMessage, parentMessage) + XCTAssertTrue(viewModel.threadMessageShown) + } + func test_chatChannelVM_crashWhenIndexAccess() { // Given let message1 = ChatMessage.mock() diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift index 9239d3ff..74a6cb9b 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift @@ -578,12 +578,54 @@ class MessageContainerView_Tests: StreamChatTestCase { assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + func test_messageContainerHighlighted_snapshot() { + // Given + let message = ChatMessage.mock( + id: "test-message-id", + cid: .unique, + text: "This message is highlighted", + author: .mock(id: .unique, name: "Test User"), + isSentByCurrentUser: false + ) + let messageId = message.messageId + + // When + let view = testMessageViewContainer( + message: message, + highlightedMessageId: messageId + ) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_messageContainerNotHighlighted_snapshot() { + // Given + let message = ChatMessage.mock( + id: "test-message-id", + cid: .unique, + text: "This message is not highlighted", + author: .mock(id: .unique, name: "Test User"), + isSentByCurrentUser: false + ) + + // When + let view = testMessageViewContainer( + message: message, + highlightedMessageId: "different-message-id" + ) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + // MARK: - private func testMessageViewContainer( message: ChatMessage, channel: ChatChannel? = nil, - messageViewModel: MessageViewModel? = nil + messageViewModel: MessageViewModel? = nil, + highlightedMessageId: String? = nil ) -> some View { MessageContainerView( factory: DefaultViewFactory.shared, @@ -598,6 +640,7 @@ class MessageContainerView_Tests: StreamChatTestCase { onLongPress: { _ in }, viewModel: messageViewModel ?? MessageViewModel(message: message, channel: channel) ) + .environment(\.highlightedMessageId, highlightedMessageId) .frame(width: 375, height: 200) } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png new file mode 100644 index 0000000000000000000000000000000000000000..d7de7e4bbf085e06294c8167e2d165d26a55b9f9 GIT binary patch literal 28863 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 z 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 literal 0 HcmV?d00001 From 1a6564a7b899c4d35977ada92b890841d5d18e54 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 5 Nov 2025 18:05:28 +0000 Subject: [PATCH 06/11] Fix jumping to message highlight to be consistent with UIKit (#1036) --- CHANGELOG.md | 2 +- .../ChatChannel/ChatChannelViewModel.swift | 14 ++++++++++---- .../MessageList/MessageContainerView.swift | 4 +++- .../MessageList/MessageListConfig.swift | 7 +++++++ Sources/StreamChatSwiftUI/ColorPalette.swift | 5 ++--- ...messageContainerHighlighted_snapshot.1.png | Bin 28863 -> 28809 bytes 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2342e94..3448f84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming ### ✅ Added -- Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1030) +- Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1032) ### 🐞 Fixed - Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index f37c6722..76185ec4 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -183,7 +183,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } // Clear highlight after animation completes DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in - self?.highlightedMessageId = nil + withAnimation { + self?.highlightedMessageId = nil + } } self?.messageCachingUtils.jumpToReplyId = nil } else if messageController == nil { @@ -324,9 +326,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.scrolledId = nil } - // Clear highlight after animation completes (0.6s delay from StreamChatUI implementation) + // Clear highlight after animation completes DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in - self?.highlightedMessageId = nil + withAnimation { + self?.highlightedMessageId = nil + } } return true } else { @@ -362,7 +366,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } // Clear highlight after animation completes DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { - self?.highlightedMessageId = nil + withAnimation { + self?.highlightedMessageId = nil + } } } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift index 656faf46..cf9324c9 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift @@ -287,7 +287,9 @@ public struct MessageContainerView: View { .padding(.top, isLast ? paddingValue : 0) .background( Group { - if let highlightedMessageId = highlightedMessageId, highlightedMessageId == message.messageId { + if utils.messageListConfig.highlightMessageWhenJumping, + let highlightedMessageId = highlightedMessageId, + highlightedMessageId == message.messageId { Color(colors.messageCellHighlightBackground) } else if messageViewModel.isPinned { Color(colors.pinnedBackground) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift index b03d8550..2339728d 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift @@ -26,6 +26,7 @@ public struct MessageListConfig { iPadSplitViewEnabled: Bool = true, scrollingAnchor: UnitPoint = .center, showNewMessagesSeparator: Bool = true, + highlightMessageWhenJumping: Bool = true, handleTabBarVisibility: Bool = true, messageListAlignment: MessageListAlignment = .standard, uniqueReactionsEnabled: Bool = false, @@ -57,6 +58,7 @@ public struct MessageListConfig { self.iPadSplitViewEnabled = iPadSplitViewEnabled self.scrollingAnchor = scrollingAnchor self.showNewMessagesSeparator = showNewMessagesSeparator + self.highlightMessageWhenJumping = highlightMessageWhenJumping self.handleTabBarVisibility = handleTabBarVisibility self.messageListAlignment = messageListAlignment self.uniqueReactionsEnabled = uniqueReactionsEnabled @@ -121,6 +123,11 @@ public struct MessageListConfig { /// A boolean value that determines if download action is shown for file attachments. public let downloadFileAttachmentsEnabled: Bool + + /// Highlights the message background when jumping to a message. + /// + /// By default it is enabled and it uses the color from `ColorPalette.messageCellHighlightBackground`. + public let highlightMessageWhenJumping: Bool } /// Contains information about the message paddings. diff --git a/Sources/StreamChatSwiftUI/ColorPalette.swift b/Sources/StreamChatSwiftUI/ColorPalette.swift index e38841bb..ec71b5ec 100644 --- a/Sources/StreamChatSwiftUI/ColorPalette.swift +++ b/Sources/StreamChatSwiftUI/ColorPalette.swift @@ -59,7 +59,7 @@ public struct ColorPalette { public var highlightedBackground: UIColor = .streamGrayGainsboro public var highlightedAccentBackground: UIColor = .streamAccentBlue public var highlightedAccentBackground1: UIColor = .streamBlueAlice - public var pinnedBackground: UIColor = .streamHighlight + public var pinnedBackground: UIColor = .streamYellowBackground public var messageCellHighlightBackground: UIColor = .streamYellowBackground // MARK: - Borders and shadows @@ -167,8 +167,7 @@ private extension UIColor { static let streamAccentGreen = mode(0x20e070, 0x20e070) static let streamGrayDisabledText = mode(0x72767e, 0x72767e) static let streamInnerBorder = mode(0xdbdde1, 0x272a30) - static let streamHighlight = mode(0xfbf4dd, 0x333024) - static let streamYellowBackground = mode(0xfff2a1, 0x4a3d00) + static let streamYellowBackground = mode(0xfbf4dd, 0x333024) static let streamDisabled = mode(0xb4b7bb, 0x4c525c) // Currently we are not using the correct shadow color from figma's color palette. This is to avoid diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png index d7de7e4bbf085e06294c8167e2d165d26a55b9f9..fba36a5453c4a770e9a81236ea196c308826fdbc 100644 GIT binary patch literal 28809 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 literal 28863 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 t*Bf%-fs1 Date: Thu, 6 Nov 2025 12:55:54 +0000 Subject: [PATCH 07/11] Display double grey checkmark when delivery events are enabled (#1038) * Display double grey checkmark when delivery events are enabled * Update CHANGELOG.md * Add MessageReadIndicatorView_Tests * Fix tests compilations * Add test coverage to the channelListItem * [CI] Snapshots (#1039) Co-authored-by: Stream Bot --------- Co-authored-by: Stream SDK Bot <60655709+Stream-SDK-Bot@users.noreply.github.com> Co-authored-by: Stream Bot --- CHANGELOG.md | 1 + .../MessageList/MessageListHelperViews.swift | 11 ++- .../ChatChannelList/ChatChannelListItem.swift | 10 +- .../DefaultViewFactory.swift | 2 + StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 +- .../ChatChannelInfoViewModel_Tests.swift | 2 +- .../ChatChannelExtensions_Tests.swift | 9 +- .../MessageReadIndicatorView_Tests.swift | 26 ++++++ ...dicatorView_snapshotMessageDelivered.1.png | Bin 0 -> 1117 bytes ...View_snapshotMessageDeliveredAndRead.1.png | Bin 0 -> 2029 bytes .../ChatChannelListItemView_Tests.swift | 86 ++++++++++++++++++ ...est_channelListItem_messageDelivered.1.png | Bin 0 -> 18839 bytes ...nnelListItem_messageDeliveredAndRead.1.png | Bin 0 -> 18847 bytes 13 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDelivered.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDeliveredAndRead.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDelivered.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDeliveredAndRead.1.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 3448f84b..a7f279fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### ✅ Added - Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1032) +- Display double grey checkmark when delivery events are enabled [#1038](https://github.com/GetStream/stream-chat-swiftui/pull/1038) ### 🐞 Fixed - Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift index cdee7b8a..f4dd7dc2 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift @@ -100,11 +100,18 @@ public struct MessageReadIndicatorView: View { var readUsers: [ChatUser] var showReadCount: Bool + var showDelivered: Bool var localState: LocalMessageState? - public init(readUsers: [ChatUser], showReadCount: Bool, localState: LocalMessageState? = nil) { + public init( + readUsers: [ChatUser], + showReadCount: Bool, + showDelivered: Bool = false, + localState: LocalMessageState? = nil + ) { self.readUsers = readUsers self.showReadCount = showReadCount + self.showDelivered = showDelivered self.localState = localState } @@ -135,7 +142,7 @@ public struct MessageReadIndicatorView: View { } private var image: UIImage { - shouldShowReads ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent) + shouldShowReads || showDelivered ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent) } private var isMessageSending: Bool { diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift index e73a639b..09da8232 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift @@ -85,9 +85,10 @@ public struct ChatChannelListItem: View { MessageReadIndicatorView( readUsers: channel.readUsers( currentUserId: chatClient.currentUserId, - message: channel.latestMessages.first + message: channel.previewMessage ), - showReadCount: false + showReadCount: false, + showDelivered: channel.previewMessage?.deliveryStatus(for: channel) == .delivered ) } SubtitleText(text: injectedChannelInfo?.timestamp ?? channel.timestampText) @@ -159,9 +160,8 @@ public struct ChatChannelListItem: View { } private var shouldShowReadEvents: Bool { - if let message = channel.latestMessages.first, - message.isSentByCurrentUser, - !message.isDeleted { + if let message = channel.previewMessage, + message.isSentByCurrentUser { return channel.config.readEventsEnabled } diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index 19e18fd3..348993bb 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -1027,9 +1027,11 @@ extension ViewFactory { message: message ) let showReadCount = channel.memberCount > 2 && !message.isLastActionFailed + let showDelivered = message.deliveryStatus(for: channel) == .delivered return MessageReadIndicatorView( readUsers: readUsers, showReadCount: showReadCount, + showDelivered: showDelivered, localState: message.localState ) } diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index 904fe0c4..66dd50e4 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3945,8 +3945,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.91.0; + branch = release/4.92.0; + kind = branch; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift index de6a0d03..668ae825 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift @@ -389,7 +389,7 @@ class ChatChannelInfoViewModel_Tests: StreamChatTestCase { let mutedUser = ChatUser.mock(id: .unique) let viewModel = ChatChannelInfoViewModel(channel: channel) let currentUserController = CurrentChatUserController_Mock(client: chatClient) - let currentUser = CurrentChatUser.mock(id: .unique, mutedUsers: [mutedUser]) + let currentUser = CurrentChatUser.mock(currentUserId: .unique, mutedUsers: [mutedUser]) currentUserController.currentUser_mock = currentUser viewModel.currentUserController = currentUserController diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift index 1c58eabd..7b5e5b87 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift @@ -60,7 +60,14 @@ class ChatChannelExtensions_Tests: StreamChatTestCase { // Given let user = ChatUser.mock(id: .unique) let messages = [ChatMessage.mock(id: .unique, cid: .unique, text: "Test", author: ChatUser.mock(id: .unique))] - let read = ChatChannelRead(lastReadAt: Date(), lastReadMessageId: nil, unreadMessagesCount: 0, user: user) + let read = ChatChannelRead( + lastReadAt: Date(), + lastReadMessageId: nil, + unreadMessagesCount: 0, + user: user, + lastDeliveredAt: nil, + lastDeliveredMessageId: nil + ) let channel = ChatChannel.mockDMChannel(reads: [read], latestMessages: messages) // When diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift index e3edd1b2..5695b7fd 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift @@ -122,4 +122,30 @@ class MessageReadIndicatorView_Tests: StreamChatTestCase { // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + + func test_messageReadIndicatorView_snapshotMessageDelivered() { + // Given + let view = MessageReadIndicatorView( + readUsers: [], + showReadCount: false, + showDelivered: true + ) + .frame(width: 50, height: 16) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_messageReadIndicatorView_snapshotMessageDeliveredAndRead() { + // Given + let view = MessageReadIndicatorView( + readUsers: [.mock(id: .unique), .mock(id: .unique)], + showReadCount: true, + showDelivered: true + ) + .frame(width: 50, height: 16) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDelivered.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDelivered.1.png new file mode 100644 index 0000000000000000000000000000000000000000..9f0030a36a7f3fef4871fbf9cbff9be5c52bbd1b GIT binary patch literal 1117 zcmeAS@N?(olHy`uVBq!ia0vp^(}383gAGW2c4-y_QjEnx?oJHr&dIz4a#~U&JkxxA z8MJ_G4hB|6AqHlU5+Gz?lwx27vl$q?7^UItAVv+S8YTvY_DlvAsG2As4FWfS7^oXc zGcRC7n7Dumu6o)6W;ojbq;09F`$r(fS>O>_%)p>%0m6)~(+m@Uf-;#d{vkjbXdxI# z07(rX2GKywc!DuOO&&;3@pN$vsbG9_{kGpB2LZN(d6CK$9VY@miaeI?ybz|%p%_%f z@Tl0t*X{a}-f6L3+0Pc8_CLgW`=;XG@24HKTf~JDJQf_fAgnu)fl*9gg9<~0&c;QW z2?rUxnHr7?Y0qL3;~`e?nmx~tgI&A!vad2<|FYKT^qVbF^_Q-mRi0~JmC~;^^J}Ue z!k4yN#A3o+A~86|6Zt=dhPY5j?Sr%RYF&^EtBcw zx_Wu@#K+O6Be^21=gr>y;oqO-UTXbJQ=jWEotz~f(CNXL%^sl@(w6C6zB+K`r7Br* zM(xKT&$~QRBYp;aW&hpm<+93D=Q{U_t9uu_J)gF0b3c#M?CH<@wEzBOUUhcOCGDug zS@HMO{gd0ba!d6tfB8GSE^MLETJx83jS=6zT$hiyRCQN;KQM~^|8G2Z_z^IER)ON@ z?G0G$++>#1vzb`&eD(~9!@Zq`fjUbAIxiUiGSE(*la{!AD)+Nxzs(hcZ{79%_Wbmg zh3Qr`JbCZWIR0PsPi9`stcra<%8#Gd&;Ke@T_d<|7zarXI{m$WbNKvxcyO9`Pxmtgar5XH+C6)XA;XTnfZiIrboyp`t&kKh< za~isYa_#ptx*F;Pi;I^kGaZZV%r==ld(Vm^>r@)Kk6r$@#CG><(KTi^U;wbWR7_F`-F?fCT}T_>*hJ?c2sfeLnbZ1%*|Dt-AW2EJ)Nstx*ls?o7;W7 z@aW_I%Z01AZ_wcSr99={^cS+P3x7OWwz>3asmQg*@8#Q~%RgTAJCGewCSY+tpQ&N% zeVdK9;+Yy)xwJSLrg^Q25ZRF6pvrh4s8h5Xn0{9P6SdaHq`(7Bj8j<{yj8<=xFQS| sOkzk_d8Esgk((_)78&qol`;+01no#>;M1& literal 0 HcmV?d00001 diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDeliveredAndRead.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDeliveredAndRead.1.png new file mode 100644 index 0000000000000000000000000000000000000000..1a3d812fd00319461dd2a9787b910497b2a37370 GIT binary patch literal 2029 zcmYL|dpy(oAIHDDvAJZ}L`o_}sHyk~4}@uKjhT%hnmVpaE}cv+*)qAMlv*hLW|Fbe zI=NI9JHHcMgoAc+Db^H`PSlDBzpwM?(fNEHulMuy`8;0l&*!gq9>s^Ct+7S}003=o zFFX|_ACwJJ4XmH>S-Kz~N2vsNpss7}1bE;@FuWtlWWW;iNB{xR17Ir=a08Gv0OB7H z07QuKPfvwd{K!B7ASVWZ|HuS`Tv`2~!0sP}2_XNzD1iP<^98V<-e#q(3QL#)flBZS zItldY&Og(ofYMT)H4IYq(7CH3KHHJ4n)gV@7Wxq;FHu?SX%Ujb3K$34WeNSWD0~^J}S{YTVU&w)W7zEL&2>wBt0RqEld|Z3pQi6dsS}0;A!D*08;N7Z0 zVIk2`3$Y(z;PGocw1mCNFbz4q_D`45vw%1r(j?}#Oxs}m$h(9bE1AX@*uqtyGRd|` zMalbORLvn~?p67DTU(ksGo;V9NlQ#9sj|IAmv5)lC{IJFS=c;4od2w%?267lrL}jgNVlaOqwUS^)YVEyC%-TU$}-9L{&gP(5i2vzn<3qbN!%%)4B^_f$#%yd zF4g(&Gu&Jj$()H`x^GWd+Ke{wGWn?TtNA_H)SA0#!bCjXaPOC@mWK&+`?97)EmWCOm!3$?wdBwBXNt!NO~<8LglmZRtPp9@6y7TSI{$E+_Mv8BQ_aw# z7i;~vm_sK~$kjQzJ$YD$0pWC%#A0k_yry_2nb*Jl65q#&xX!KN250_} zj~(*j>i#f|dX-_?+~4LlpX;~thtRxl7xzTV3~_y~y^aBLdu9iW`FXKFNm991 zanS~CYxrWqwOVt;V;k;T3yLyx%#Ll8HqL@Xmh0AK3!ZMR%y1k$aqfYaCZGPgQSY?J zk?9xMVOvHgzcd+=>LGc58<2-H5260NfV3>zbF_;pJuz}dc^r4k{z|FKK>22|Z1NrK z`q~0@mR9E{LDnv5;gwi>kE$Hrd_7kXd#yU?w$!}3j1+I6RHAB~;dEcG)BIU>0Jkk` z^L=?^PFG$xWd}O3W4&9$)e545rX~Nb38f8}3>`z1H?Oja>RKcD`?+C7SqXteh6Z{O zL`%qoTLvZg%aS`WLABG>eZ+!r(bt_ccrSX2c7Q;!Gkd2$(obj#RCSFZl3k#Vp=wxh z5tiw^a5#NyXI79qld)|IttD5SfB5CMvl9`l>dnWO{WsBAAE6^oryV9A*BFHMc3}}c znp38tK@`QF?+T9G@^aw8y#wI9#jecThmr*n#t-wzeqqN+q)^?H0`JlBgzJ7Gsn*lb za5poor^*A(y?#d3yYmk%k_A#Id>5*h9dyCyYnB@&TIUJj+ry~$EOKiGu1kv@890*| zG*-tBFKuRbInO?uO&_~bs_pjK_xL&+Cw*j9YU@^9b#nNtMVYiub^PVmj?t6Wg1JZi zCsybA zL!GZqJp<*7XvUfuE!z~P6|vgBTLzgod-QJL$cO!_(@|!O5ELD+OY*AVBOS?x){=J!9H$MVy#Wy~d5}wg@^#1ZK3m#Qd`-?%uJ~-i z+stXM$6fw^)vwZ_1MwN-NV;Bk_r7AK+5RUw{d+T^A$5siE>Vz$^^nfY@^xQaV-bI` zhbNRqkNRRu9+tD8H?;Op2aO@0p5A4jVrN1q=l7Q*pG_9*@xvB{?F)gvZNKc^AMNVV zXii%|(*2=sn{A(~-I)9jGqLf4nUh2FM~H_<5Q^oCoC z@ls0K&>#7mm_d)nTi#<2Ense@lb3QA$HcN1W4AvXb2Vm2Qu~wetUQDqKOPx$zuLQX z4yG9pnzKd(=c(P8%ZicZw~qB4B*6Zv_d5C=w^PDwJD_o=l8De^Sjuf%_Jgd$L<*=7 z7@ufYhzkU1m2D)V$v9P1@@NNL#(7H&SF^<0Fl%Uib4eiYQA@c0wlBe;9gD>q%1V{s P_yOLYKKMF!=E?s9Qln=s literal 0 HcmV?d00001 diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift index 2ecd33df..92c637f2 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift @@ -377,6 +377,92 @@ final class ChatChannelListItemView_Tests: StreamChatTestCase { // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + + func test_channelListItem_messageDelivered() throws { + let date = Date(timeIntervalSince1970: 100) + + // Given + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test message", + author: .mock(id: .unique, name: "Darth Vader"), + createdAt: date.addingTimeInterval(-100), + isSentByCurrentUser: true + ) + let channel = ChatChannel.mock( + cid: .unique, + config: .mock(readEventsEnabled: true), + reads: [ + .init( + lastReadAt: .distantPast, + lastReadMessageId: nil, + unreadMessagesCount: 0, + user: .unique, + lastDeliveredAt: date, + lastDeliveredMessageId: message.id + ) + ], + latestMessages: [message], + previewMessage: message + ) + + // When + let view = ChatChannelListItem( + channel: channel, + channelName: "Test", + avatar: .circleImage, + onlineIndicatorShown: false, + onItemTap: { _ in } + ) + .frame(width: defaultScreenSize.width) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_channelListItem_messageDeliveredAndRead() throws { + let date = Date(timeIntervalSince1970: 100) + + // Given + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test message", + author: .mock(id: .unique, name: "Darth Vader"), + createdAt: date.addingTimeInterval(-100), + isSentByCurrentUser: true + ) + let channel = ChatChannel.mock( + cid: .unique, + config: .mock(readEventsEnabled: true), + reads: [ + .init( + lastReadAt: date.addingTimeInterval(10), + lastReadMessageId: message.id, + unreadMessagesCount: 0, + user: .unique, + lastDeliveredAt: date, + lastDeliveredMessageId: message.id + ) + ], + latestMessages: [message], + previewMessage: message + ) + + // When + let view = ChatChannelListItem( + channel: channel, + channelName: "Test", + avatar: .circleImage, + onlineIndicatorShown: false, + onItemTap: { _ in } + ) + .frame(width: defaultScreenSize.width) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } // MARK: - private diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDelivered.1.png b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDelivered.1.png new file mode 100644 index 0000000000000000000000000000000000000000..634640fd77e2903a9dc49476afa255991e8a7356 GIT binary patch literal 18839 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 Date: Thu, 6 Nov 2025 19:32:56 +0000 Subject: [PATCH 08/11] Fix highlighting message when marking it unread (#1040) * Fix message being highlighted when marking it unread * Add additional test coverage --- .../ChatChannel/ChatChannelViewModel.swift | 59 +++++++------- .../MessageActionsResolver.swift | 1 + .../ChatChannelViewModel_Tests.swift | 78 +++++++++++++++++++ .../ChatChannel/MessageActions_Tests.swift | 1 + 4 files changed, 107 insertions(+), 32 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index 76185ec4..5b40308e 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -58,6 +58,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { @Published public var scrolledId: String? @Published public var highlightedMessageId: String? @Published public var listId = UUID().uuidString + // A boolean to skip highlighting of a message when scrolling to it. + // This is used for scenarios when scrolling to message Id should not highlight it. + var skipHighlightMessageId: String? @Published public var showScrollToLatestButton = false @Published var showAlertBanner = false @@ -173,20 +176,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { self?.messageCachingUtils.jumpToReplyId = scrollToMessage.messageId } else if messageController != nil, let jumpToReplyId = self?.messageCachingUtils.jumpToReplyId { self?.scrolledId = jumpToReplyId - // Trigger highlight when jumping to reply in thread - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.highlightedMessageId = jumpToReplyId - } // Clear scroll ID after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.scrolledId = nil } - // Clear highlight after animation completes - DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in - withAnimation { - self?.highlightedMessageId = nil - } - } + self?.highlightMessage(withId: jumpToReplyId) self?.messageCachingUtils.jumpToReplyId = nil } else if messageController == nil { self?.scrolledId = scrollToMessage?.messageId @@ -318,20 +312,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if scrolledId == nil { scrolledId = messageId } - // Trigger highlight after a short delay to allow scroll animation to start - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.highlightedMessageId = messageId - } // Clear scroll ID after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.scrolledId = nil } - // Clear highlight after animation completes - DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in - withAnimation { - self?.highlightedMessageId = nil - } - } + highlightMessage(withId: messageId) return true } else { let message = channelController.dataStore.message(id: baseId) @@ -360,23 +345,33 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.scrolledId = toJumpId self?.loadingMessagesAround = false - // Trigger highlight after scroll starts - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self?.highlightedMessageId = toJumpId - } - // Clear highlight after animation completes - DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { - withAnimation { - self?.highlightedMessageId = nil - } - } + self?.highlightMessage(withId: toJumpId) } } return false } } } - + + /// Highlights the message background. + /// + /// - Parameter messageId: The ID of the message to highlight. + public func highlightMessage(withId messageId: MessageId) { + if skipHighlightMessageId == messageId { + skipHighlightMessageId = nil + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.highlightedMessageId = messageId + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in + withAnimation { + self?.highlightedMessageId = nil + } + } + } + open func handleMessageAppear(index: Int, scrollDirection: ScrollDirection) { if index >= channelDataSource.messages.count || loadingMessagesAround { return @@ -561,7 +556,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } // MARK: - private - + private func checkForOlderMessages(index: Int) { guard index >= channelDataSource.messages.count - 25 else { return } guard !loadingPreviousMessages else { return } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift index 40f441d0..a4775057 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift @@ -40,6 +40,7 @@ public class MessageActionsResolver: MessageActionsResolving { } else if info.identifier == MessageActionId.markUnread { viewModel.firstUnreadMessageId = info.message.messageId viewModel.currentUserMarkedMessageUnread = true + viewModel.skipHighlightMessageId = info.message.messageId viewModel.scrolledId = info.message.messageId } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift index 8b267172..ace127d7 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift @@ -772,6 +772,84 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { XCTAssertEqual(0, channelController.markReadCallCount) } + // MARK: - highlightMessage Tests + + func test_highlightMessage_highlightsWhenSkipHighlightMessageIdIsNotSet() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + let testExpectation = XCTestExpectation(description: "Message should be highlighted") + + // When + viewModel.highlightMessage(withId: message.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertEqual(viewModel.highlightedMessageId, message.messageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: defaultTimeout) + } + + func test_highlightMessage_highlightsWhenSkipHighlightMessageIdDoesNotMatch() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.skipHighlightMessageId = message1.messageId + let testExpectation = XCTestExpectation(description: "Message should be highlighted") + + // When + viewModel.highlightMessage(withId: message2.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertEqual(viewModel.highlightedMessageId, message2.messageId) + XCTAssertEqual(viewModel.skipHighlightMessageId, message1.messageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: defaultTimeout) + } + + func test_highlightMessage_doesNotHighlightWhenSkipHighlightMessageIdMatches() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.skipHighlightMessageId = message.messageId + let testExpectation = XCTestExpectation(description: "Message should not be highlighted") + + // When + viewModel.highlightMessage(withId: message.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertNil(viewModel.highlightedMessageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: defaultTimeout) + } + + func test_highlightMessage_clearsSkipHighlightMessageIdAfterSkipping() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.skipHighlightMessageId = message.messageId + + // When + viewModel.highlightMessage(withId: message.messageId) + + // Then + XCTAssertNil(viewModel.skipHighlightMessageId) + XCTAssertNil(viewModel.highlightedMessageId) + } + // MARK: - private private func makeChannelController( diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift index 56e08446..e30214df 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift @@ -418,6 +418,7 @@ class MessageActions_Tests: StreamChatTestCase { XCTAssertEqual(viewModel.firstUnreadMessageId, message.messageId) XCTAssertTrue(viewModel.currentUserMarkedMessageUnread) XCTAssertEqual(viewModel.scrolledId, message.messageId) + XCTAssertEqual(viewModel.skipHighlightMessageId, message.messageId) XCTAssertFalse(viewModel.reactionsShown) } From 15e903f46abada665b015df43826f989a9c63c35 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Thu, 6 Nov 2025 21:25:49 +0000 Subject: [PATCH 09/11] Fix mark unread action not shown for messages that are root of a thread in the channel view (#1041) * Fix mark unread action not shown for messages that are root of a thread in the channel view * Update CHANGELOG.md * Fix mark unread action not shown for messages that are root of a thread in the channel view --- CHANGELOG.md | 1 + .../DefaultMessageActions.swift | 18 ++-- .../ChatChannel/MessageActions_Tests.swift | 99 +++++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f279fc..62ad0828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🐞 Fixed - Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030) +- Fix mark unread action not shown for messages that are root of a thread in the channel view [#1041](https://github.com/GetStream/stream-chat-swiftui/pull/1041) # [4.91.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.91.0) _October 22, 2025_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift index 8e29a81e..22200fa6 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift @@ -121,16 +121,14 @@ public extension MessageAction { messageActions.append(copyAction) } - if message.isRootOfThread { - if isInsideThreadView { - let markThreadUnreadAction = markThreadAsUnreadAction( - messageController: messageController, - message: message, - onFinish: onFinish, - onError: onError - ) - messageActions.append(markThreadUnreadAction) - } + if message.isRootOfThread && isInsideThreadView { + let markThreadUnreadAction = markThreadAsUnreadAction( + messageController: messageController, + message: message, + onFinish: onFinish, + onError: onError + ) + messageActions.append(markThreadUnreadAction) } else if !message.isSentByCurrentUser && channel.canReceiveReadEvents { if !message.isPartOfThread || message.showReplyInChannel { let markUnreadAction = markAsUnreadAction( diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift index e30214df..32313044 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift @@ -72,6 +72,105 @@ class MessageActions_Tests: StreamChatTestCase { XCTAssert(messageActions[5].title == "Mute User") } + func test_messageActions_partOfThread() { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: .unique), + parentMessageId: .unique, + showReplyInChannel: false, + isSentByCurrentUser: false + ) + let factory = DefaultViewFactory.shared + + // When + let messageActions = MessageAction.defaultActions( + factory: factory, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: { _ in }, + onError: { _ in } + ) + + // Then + XCTAssertEqual(messageActions.count, 4) + XCTAssertEqual(messageActions[0].title, "Reply") + XCTAssertEqual(messageActions[1].title, "Pin to conversation") + XCTAssertEqual(messageActions[2].title, "Copy Message") + XCTAssertEqual(messageActions[3].title, "Mute User") + } + + func test_messageActions_partOfThreadButAlsoInChannel() { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: .unique), + parentMessageId: .unique, + showReplyInChannel: true, + isSentByCurrentUser: false + ) + let factory = DefaultViewFactory.shared + + // When + let messageActions = MessageAction.defaultActions( + factory: factory, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: { _ in }, + onError: { _ in } + ) + + // Then + XCTAssertEqual(messageActions.count, 5) + XCTAssertEqual(messageActions[0].title, "Reply") + XCTAssertEqual(messageActions[1].title, "Pin to conversation") + XCTAssertEqual(messageActions[2].title, "Copy Message") + XCTAssertEqual(messageActions[3].title, "Mark Unread") + XCTAssertEqual(messageActions[4].title, "Mute User") + } + + func test_messageActions_rootOfThreadButAlsoInChannel() { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: .unique), + parentMessageId: .unique, + showReplyInChannel: true, + replyCount: 3, + isSentByCurrentUser: false + ) + let factory = DefaultViewFactory.shared + + // When + let messageActions = MessageAction.defaultActions( + factory: factory, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: { _ in }, + onError: { _ in } + ) + + // Then + XCTAssertEqual(messageActions.count, 5) + XCTAssertEqual(messageActions[0].title, "Reply") + XCTAssertEqual(messageActions[1].title, "Pin to conversation") + XCTAssertEqual(messageActions[2].title, "Copy Message") + XCTAssertEqual(messageActions[3].title, "Mark Unread") + XCTAssertEqual(messageActions[4].title, "Mute User") + } + func test_messageActions_otherUserDefaultReadEventsDisabled() { // Given let channel = ChatChannel.mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage]) From 09097343bb715a9b1400517b102ac4c88ddceebb Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Fri, 7 Nov 2025 16:03:42 +0000 Subject: [PATCH 10/11] Update StreamChat dependency to 4.92.0 (#1043) --- Package.swift | 2 +- StreamChatSwiftUI-XCFramework.podspec | 2 +- StreamChatSwiftUI.podspec | 2 +- StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index aa0f4fef..ddd1fb18 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", from: "4.91.0") + .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.92.0") ], targets: [ .target( diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 6d88ce8d..149a6b40 100644 --- a/StreamChatSwiftUI-XCFramework.podspec +++ b/StreamChatSwiftUI-XCFramework.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |spec| spec.framework = 'Foundation', 'UIKit', 'SwiftUI' - spec.dependency 'StreamChat-XCFramework', '~> 4.91.0' + spec.dependency 'StreamChat-XCFramework', '~> 4.92.0' spec.cocoapods_version = '>= 1.11.0' end diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index e8cd2f44..a8cbd379 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -19,5 +19,5 @@ Pod::Spec.new do |spec| spec.framework = 'Foundation', 'UIKit', 'SwiftUI' - spec.dependency 'StreamChat', '~> 4.91.0' + spec.dependency 'StreamChat', '~> 4.92.0' end diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index 66dd50e4..cb726deb 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3945,8 +3945,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - branch = release/4.92.0; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 4.92.0; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { From d971b8896af71d46fe2b0c87ba2a88e83d4f34aa Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Fri, 7 Nov 2025 16:25:16 +0000 Subject: [PATCH 11/11] Bump 4.92.0 --- CHANGELOG.md | 5 +++++ README.md | 2 +- .../Generated/SystemEnvironment+Version.swift | 2 +- Sources/StreamChatSwiftUI/Info.plist | 2 +- StreamChatSwiftUI-XCFramework.podspec | 2 +- StreamChatSwiftUI.podspec | 2 +- StreamChatSwiftUIArtifacts.json | 2 +- 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ad0828..dc1a1d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🔄 Changed + +# [4.92.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.92.0) +_November 07, 2025_ + ### ✅ Added - Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1032) - Display double grey checkmark when delivery events are enabled [#1038](https://github.com/GetStream/stream-chat-swiftui/pull/1038) diff --git a/README.md b/README.md index e4478e92..070f3eec 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## SwiftUI StreamChat SDK diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index 05c0a678..7f2ac48c 100644 --- a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift +++ b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift @@ -7,5 +7,5 @@ import Foundation enum SystemEnvironment { /// A Stream Chat version. - public static let version: String = "4.92.0-SNAPSHOT" + public static let version: String = "4.92.0" } diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist index 268afdcc..6bc4b9c0 100644 --- a/Sources/StreamChatSwiftUI/Info.plist +++ b/Sources/StreamChatSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.91.0 + 4.92.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPhotoLibraryUsageDescription diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 149a6b40..bef1bf4d 100644 --- a/StreamChatSwiftUI-XCFramework.podspec +++ b/StreamChatSwiftUI-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI-XCFramework' - spec.version = '4.91.0' + spec.version = '4.92.0' spec.summary = 'StreamChat SwiftUI Chat Components' spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.' diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index a8cbd379..1f38e2e6 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI' - spec.version = '4.91.0' + spec.version = '4.92.0' spec.summary = 'StreamChat SwiftUI Chat Components' spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.' diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json index 84875efd..59343d47 100644 --- a/StreamChatSwiftUIArtifacts.json +++ b/StreamChatSwiftUIArtifacts.json @@ -1 +1 @@ -{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip","4.79.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.1/StreamChatSwiftUI.zip","4.80.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.80.0/StreamChatSwiftUI.zip","4.81.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.81.0/StreamChatSwiftUI.zip","4.82.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.82.0/StreamChatSwiftUI.zip","4.83.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.83.0/StreamChatSwiftUI.zip","4.84.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.84.0/StreamChatSwiftUI.zip","4.85.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.85.0/StreamChatSwiftUI.zip","4.86.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.86.0/StreamChatSwiftUI.zip","4.87.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.87.0/StreamChatSwiftUI.zip","4.88.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.88.0/StreamChatSwiftUI.zip","4.89.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.0/StreamChatSwiftUI.zip","4.89.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.1/StreamChatSwiftUI.zip","4.90.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.90.0/StreamChatSwiftUI.zip","4.91.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.91.0/StreamChatSwiftUI.zip"} \ No newline at end of file +{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip","4.79.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.1/StreamChatSwiftUI.zip","4.80.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.80.0/StreamChatSwiftUI.zip","4.81.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.81.0/StreamChatSwiftUI.zip","4.82.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.82.0/StreamChatSwiftUI.zip","4.83.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.83.0/StreamChatSwiftUI.zip","4.84.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.84.0/StreamChatSwiftUI.zip","4.85.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.85.0/StreamChatSwiftUI.zip","4.86.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.86.0/StreamChatSwiftUI.zip","4.87.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.87.0/StreamChatSwiftUI.zip","4.88.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.88.0/StreamChatSwiftUI.zip","4.89.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.0/StreamChatSwiftUI.zip","4.89.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.1/StreamChatSwiftUI.zip","4.90.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.90.0/StreamChatSwiftUI.zip","4.91.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.91.0/StreamChatSwiftUI.zip","4.92.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.92.0/StreamChatSwiftUI.zip"} \ No newline at end of file

- StreamChatSwiftUI + StreamChatSwiftUI