diff --git a/CHANGELOG.md b/CHANGELOG.md index 4107f8bd..c651cc4b 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/). - Rare crash when accessing frame of the view [#607](https://github.com/GetStream/stream-chat-swiftui/pull/607) - `ChatChannelListView` navigation did not trigger when using a custom container and its body reloaded [#609](https://github.com/GetStream/stream-chat-swiftui/pull/609) - Channel was sometimes not marked as read when tapping the x on the unread message pill in the message list [#610](https://github.com/GetStream/stream-chat-swiftui/pull/610) +- Channel was sometimes not selected if `ChatChannelViewModel.selectedChannelId` was set to a channel created a moments ago [#611](https://github.com/GetStream/stream-chat-swiftui/pull/611) - Fix the poll vote progress view not having full width when the Poll is closed [#612](https://github.com/GetStream/stream-chat-swiftui/pull/612) - Fix the last vote author not accurate in the channel preview [#612](https://github.com/GetStream/stream-chat-swiftui/pull/612) diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift index 2265d8a4..1fe060e3 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift @@ -2,6 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // +import Combine import Foundation import StreamChat import SwiftUI @@ -117,7 +118,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController public var isSearching: Bool { !searchText.isEmpty } - + + /// Creates a view model for the `ChatChannelListView`. + /// + /// - Parameters: + /// - channelListController: A controller providing the list of channels. If nil, a controller with default `ChannelListQuery` is created. + /// - selectedChannelId: The id of a channel to select. If the channel is not part of the channel list query, no channel is selected. + /// Consider using ``ChatChannelScreen`` for presenting channels what might not be part of the initial page of channels. public init( channelListController: ChatChannelListController? = nil, selectedChannelId: String? = nil @@ -265,15 +272,30 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController } } + private var deeplinkCancellable: AnyCancellable? + + /// Checks for currently loaded channels for opening a channel with id. private func checkForDeeplinks() { - if let selectedChannelId = selectedChannelId, - let channelId = try? ChannelId(cid: selectedChannelId) { - let chatController = chatClient.channelController( - for: channelId, - messageOrdering: .topToBottom - ) - selectedChannel = chatController.channel?.channelSelectionInfo - self.selectedChannelId = nil + guard let selectedChannelId else { return } + do { + let channelId = try ChannelId(cid: selectedChannelId) + if let channel = channels.first(where: { $0.cid == channelId }) { + selectedChannel = channel.channelSelectionInfo + } else { + // Start waiting for a channel list change because the channel is not part of the loaded list + deeplinkCancellable = $channels + .map { Array($0) } + .compactMap { channels in + channels.first(where: { $0.cid == channelId }) + } + .map(\.channelSelectionInfo) + .sink { [weak self] selection in + self?.deeplinkCancellable = nil + self?.selectedChannel = selection + } + } + } catch { + log.error("Failed to select a channel with id \(selectedChannelId) (\(error))") } } diff --git a/StreamChatSwiftUITests/Infrastructure/Shared/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/StreamChatSwiftUITests/Infrastructure/Shared/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents index 3d988576..aca907f8 100644 --- a/StreamChatSwiftUITests/Infrastructure/Shared/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents +++ b/StreamChatSwiftUITests/Infrastructure/Shared/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents @@ -1,9 +1,11 @@ - + + + @@ -20,6 +22,7 @@ + @@ -36,10 +39,12 @@ + + @@ -132,6 +137,7 @@ + @@ -139,6 +145,7 @@ + @@ -323,10 +330,10 @@ - + - + @@ -345,7 +352,7 @@ - + @@ -372,9 +379,16 @@ + + + + + + + @@ -434,7 +448,7 @@ - + diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift index 3f6fde34..194d4fe8 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift @@ -4,6 +4,7 @@ @testable import StreamChat @testable import StreamChatSwiftUI +@testable import StreamChatTestTools import XCTest class ChatChannelListViewModel_Tests: StreamChatTestCase { @@ -270,6 +271,59 @@ class ChatChannelListViewModel_Tests: StreamChatTestCase { // Then XCTAssert(viewModel.hideTabBar == true) } + + func test_channelListVM_deeplinkToExistingChannel() throws { + // Given + let channels = (0..<3).map { ChatChannel.mock(cid: ChannelId(type: .messaging, id: "\($0)")) } + let channelListController = makeChannelListController(channels: channels) + let selectedId = channels[1].cid + let viewModel = ChatChannelListViewModel( + channelListController: channelListController, + selectedChannelId: selectedId.rawValue + ) + + // Then + let expectation = XCTestExpectation(description: "SelectedChannel") + let cancellable = viewModel.$selectedChannel + .filter { $0?.channel.cid == selectedId } + .sink { _ in + expectation.fulfill() + } + // Resume synchronize() + chatClient.mockAPIClient.test_simulateResponse(.success(ChannelListPayload(channels: []))) + wait(for: [expectation], timeout: defaultTimeout) + cancellable.cancel() + } + + func test_channelListVM_deeplinkToIncomingChannel() { + // Given + let channels = (0..<3).map { ChatChannel.mock(cid: ChannelId(type: .messaging, id: "\($0)")) } + let channelListController = makeChannelListController(channels: channels) + let selectedId = ChannelId(type: .messaging, id: "3") + let viewModel = ChatChannelListViewModel( + channelListController: channelListController, + selectedChannelId: selectedId.rawValue + ) + + // When + let expectation = XCTestExpectation(description: "SelectedChannel") + let cancellable = viewModel.$selectedChannel + .filter { $0?.channel.cid == selectedId } + .sink { _ in + expectation.fulfill() + } + let insertedChannel = ChatChannel.mock(cid: selectedId) + channelListController.simulate( + channels: channels + [insertedChannel], + changes: [.insert(insertedChannel, index: IndexPath(item: 0, section: 0))] + ) + // Resume synchronize() + chatClient.mockAPIClient.test_simulateResponse(.success(ChannelListPayload(channels: []))) + + // Then + wait(for: [expectation], timeout: defaultTimeout) + cancellable.cancel() + } // MARK: - private