diff --git a/CHANGELOG.md b/CHANGELOG.md index a7bb67d6..8c81aa71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🐞 Fixed - Fix `Throttler` crash in `ChatChannelViewModel.handleMessageAppear()` [#1050](https://github.com/GetStream/stream-chat-swiftui/pull/1050) - Remove unnecessary channel query call when leaving the channel view in a mid-page [#1050](https://github.com/GetStream/stream-chat-swiftui/pull/1050) +- Fix crash when force unwrapping `messageDisplayInfo` in `ChatChannelView` [#1052](https://github.com/GetStream/stream-chat-swiftui/pull/1052) # [4.92.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.92.0) _November 07, 2025_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index 17cb5476..1e4ee9c3 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -145,7 +145,7 @@ public struct ChatChannelView: View, KeyboardReadable { .opacity(0) // Fixes showing accessibility button shape } .overlay( - viewModel.reactionsShown ? + viewModel.currentSnapshot != nil && messageDisplayInfo != nil && viewModel.reactionsShown ? factory.makeReactionsOverlayView( channel: channel, currentSnapshot: viewModel.currentSnapshot!, diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelView_Tests.swift index 8ee7f36e..530106b7 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelView_Tests.swift @@ -167,4 +167,76 @@ class ChatChannelView_Tests: StreamChatTestCase { // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + + // MARK: - Reactions Overlay Tests + + func test_chatChannelView_doesNotCrash_whenCurrentSnapshotIsNil_andReactionsShownIsTrue() { + // Given + let controller = ChatChannelController_Mock.mock( + channelQuery: .init(cid: .unique), + channelListQuery: nil, + client: chatClient + ) + let mockChannel = ChatChannel.mock(cid: .unique, name: "Test channel") + let message = ChatMessage.mock( + id: .unique, + cid: mockChannel.cid, + text: "Test message", + author: .mock(id: .unique, name: "User") + ) + controller.simulateInitial(channel: mockChannel, messages: [message], state: .remoteDataFetched) + + let viewModel = ChatChannelViewModel(channelController: controller) + + // When + viewModel.currentSnapshot = nil + viewModel.reactionsShown = true + + let view = ChatChannelView( + viewFactory: DefaultViewFactory.shared, + viewModel: viewModel, + channelController: controller + ) + + // Then - Should not crash when rendering + let hostingController = UIHostingController(rootView: view) + XCTAssertNotNil(hostingController.view) + XCTAssertNil(viewModel.currentSnapshot) + XCTAssertTrue(viewModel.reactionsShown) + } + + func test_chatChannelView_doesNotCrash_whenMessageDisplayInfoIsNil_andReactionsShownIsTrue() { + // Given + let controller = ChatChannelController_Mock.mock( + channelQuery: .init(cid: .unique), + channelListQuery: nil, + client: chatClient + ) + let mockChannel = ChatChannel.mock(cid: .unique, name: "Test channel") + let message = ChatMessage.mock( + id: .unique, + cid: mockChannel.cid, + text: "Test message", + author: .mock(id: .unique, name: "User") + ) + controller.simulateInitial(channel: mockChannel, messages: [message], state: .remoteDataFetched) + + let viewModel = ChatChannelViewModel(channelController: controller) + + // When + viewModel.showReactionOverlay(for: AnyView(EmptyView())) + // messageDisplayInfo remains nil (not set) + + let view = ChatChannelView( + viewFactory: DefaultViewFactory.shared, + viewModel: viewModel, + channelController: controller + ) + + // Then - Should not crash when rendering + let hostingController = UIHostingController(rootView: view) + XCTAssertNotNil(hostingController.view) + XCTAssertNotNil(viewModel.currentSnapshot) + XCTAssertTrue(viewModel.reactionsShown) + } }