Skip to content

Commit daa05d6

Browse files
Channel was sometimes not selected when setting selectedChannelId (#611)
--------- Co-authored-by: Martin Mitrevski <[email protected]>
1 parent 20a2d4a commit daa05d6

File tree

4 files changed

+105
-14
lines changed

4 files changed

+105
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
- Rare crash when accessing frame of the view [#607](https://github.com/GetStream/stream-chat-swiftui/pull/607)
1010
- `ChatChannelListView` navigation did not trigger when using a custom container and its body reloaded [#609](https://github.com/GetStream/stream-chat-swiftui/pull/609)
1111
- 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)
12+
- 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)
1213
- 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)
1314
- Fix the last vote author not accurate in the channel preview [#612](https://github.com/GetStream/stream-chat-swiftui/pull/612)
1415

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright © 2024 Stream.io Inc. All rights reserved.
33
//
44

5+
import Combine
56
import Foundation
67
import StreamChat
78
import SwiftUI
@@ -117,7 +118,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
117118
public var isSearching: Bool {
118119
!searchText.isEmpty
119120
}
120-
121+
122+
/// Creates a view model for the `ChatChannelListView`.
123+
///
124+
/// - Parameters:
125+
/// - channelListController: A controller providing the list of channels. If nil, a controller with default `ChannelListQuery` is created.
126+
/// - selectedChannelId: The id of a channel to select. If the channel is not part of the channel list query, no channel is selected.
127+
/// Consider using ``ChatChannelScreen`` for presenting channels what might not be part of the initial page of channels.
121128
public init(
122129
channelListController: ChatChannelListController? = nil,
123130
selectedChannelId: String? = nil
@@ -265,15 +272,30 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
265272
}
266273
}
267274

275+
private var deeplinkCancellable: AnyCancellable?
276+
277+
/// Checks for currently loaded channels for opening a channel with id.
268278
private func checkForDeeplinks() {
269-
if let selectedChannelId = selectedChannelId,
270-
let channelId = try? ChannelId(cid: selectedChannelId) {
271-
let chatController = chatClient.channelController(
272-
for: channelId,
273-
messageOrdering: .topToBottom
274-
)
275-
selectedChannel = chatController.channel?.channelSelectionInfo
276-
self.selectedChannelId = nil
279+
guard let selectedChannelId else { return }
280+
do {
281+
let channelId = try ChannelId(cid: selectedChannelId)
282+
if let channel = channels.first(where: { $0.cid == channelId }) {
283+
selectedChannel = channel.channelSelectionInfo
284+
} else {
285+
// Start waiting for a channel list change because the channel is not part of the loaded list
286+
deeplinkCancellable = $channels
287+
.map { Array($0) }
288+
.compactMap { channels in
289+
channels.first(where: { $0.cid == channelId })
290+
}
291+
.map(\.channelSelectionInfo)
292+
.sink { [weak self] selection in
293+
self?.deeplinkCancellable = nil
294+
self?.selectedChannel = selection
295+
}
296+
}
297+
} catch {
298+
log.error("Failed to select a channel with id \(selectedChannelId) (\(error))")
277299
}
278300
}
279301

StreamChatSwiftUITests/Infrastructure/Shared/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23C71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
2+
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
33
<entity name="AttachmentDTO" representedClassName="AttachmentDTO" syncable="YES">
44
<attribute name="data" attributeType="Binary"/>
55
<attribute name="id" attributeType="String"/>
6+
<attribute name="localDownloadStateRaw" optional="YES" attributeType="String"/>
67
<attribute name="localProgress" attributeType="Double" minValueString="0" maxValueString="1" defaultValueString="0.0" usesScalarValueType="YES"/>
8+
<attribute name="localRelativePath" optional="YES" attributeType="String"/>
79
<attribute name="localStateRaw" optional="YES" attributeType="String"/>
810
<attribute name="localURL" optional="YES" attributeType="URI"/>
911
<attribute name="type" optional="YES" attributeType="String"/>
@@ -20,6 +22,7 @@
2022
<attribute name="maxMessageLength" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
2123
<attribute name="messageRetention" attributeType="String" defaultValueString=""/>
2224
<attribute name="mutesEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
25+
<attribute name="pollsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
2326
<attribute name="quotesEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
2427
<attribute name="reactionsEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
2528
<attribute name="readEventsEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -36,10 +39,12 @@
3639
<attribute name="cid" attributeType="String"/>
3740
<attribute name="cooldownDuration" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
3841
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
42+
<attribute name="currentUserUnreadMessagesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
3943
<attribute name="defaultSortingAt" attributeType="Date" usesScalarValueType="NO" spotlightIndexingEnabled="YES"/>
4044
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
4145
<attribute name="extraData" attributeType="Binary"/>
4246
<attribute name="imageURL" optional="YES" attributeType="URI"/>
47+
<attribute name="isBlocked" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
4348
<attribute name="isFrozen" attributeType="Boolean" usesScalarValueType="YES"/>
4449
<attribute name="isHidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
4550
<attribute name="lastMessageAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@@ -132,13 +137,15 @@
132137
<relationship name="channelConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ChannelConfigDTO" inverseName="commands" inverseEntity="ChannelConfigDTO"/>
133138
</entity>
134139
<entity name="CurrentUserDTO" representedClassName="CurrentUserDTO" syncable="YES">
140+
<attribute name="blockedUserIds" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer"/>
135141
<attribute name="isInvisible" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
136142
<attribute name="isReadReceiptsEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
137143
<attribute name="isTypingIndicatorsEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
138144
<attribute name="lastSynchedEventDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
139145
<attribute name="uniquenessKey" attributeType="String" defaultValueString="this is an immmutable arbitrary key which makes sure we have only once instance of CurrentUserDTO in the db"/>
140146
<attribute name="unreadChannelsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
141147
<attribute name="unreadMessagesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
148+
<attribute name="unreadThreadsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
142149
<relationship name="channelMutes" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ChannelMuteDTO" inverseName="currentUser" inverseEntity="ChannelMuteDTO"/>
143150
<relationship name="currentDevice" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceDTO" inverseName="relationship" inverseEntity="DeviceDTO"/>
144151
<relationship name="devices" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="DeviceDTO" inverseName="user" inverseEntity="DeviceDTO"/>
@@ -323,10 +330,10 @@
323330
<attribute name="voteCountsByOption" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData"/>
324331
<attribute name="votingVisibility" optional="YES" attributeType="String"/>
325332
<relationship name="createdBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserDTO" inverseName="pollCreatedBy" inverseEntity="UserDTO"/>
326-
<relationship name="latestAnswers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteDTO" inverseName="poll" inverseEntity="PollVoteDTO"/>
333+
<relationship name="latestVotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteDTO" inverseName="poll" inverseEntity="PollVoteDTO"/>
327334
<relationship name="latestVotesByOption" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="pollLatestVotes" inverseEntity="PollOptionDTO"/>
328335
<relationship name="message" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="poll" inverseEntity="MessageDTO"/>
329-
<relationship name="options" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="poll" inverseEntity="PollOptionDTO"/>
336+
<relationship name="options" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PollOptionDTO" inverseName="poll" inverseEntity="PollOptionDTO"/>
330337
</entity>
331338
<entity name="PollOptionDTO" representedClassName="PollOptionDTO" syncable="YES">
332339
<attribute name="custom" optional="YES" attributeType="Binary"/>
@@ -345,7 +352,7 @@
345352
<attribute name="pollId" attributeType="String"/>
346353
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
347354
<relationship name="option" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="latestVotes" inverseEntity="PollOptionDTO"/>
348-
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="latestAnswers" inverseEntity="PollDTO"/>
355+
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="latestVotes" inverseEntity="PollDTO"/>
349356
<relationship name="queries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteListQueryDTO" inverseName="votes" inverseEntity="PollVoteListQueryDTO"/>
350357
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserDTO" inverseName="votes" inverseEntity="UserDTO"/>
351358
</entity>
@@ -372,9 +379,16 @@
372379
<fetchIndex name="filterHash">
373380
<fetchIndexElement property="filterHash" type="Binary" order="ascending"/>
374381
</fetchIndex>
382+
<uniquenessConstraints>
383+
<uniquenessConstraint>
384+
<constraint value="filterHash"/>
385+
</uniquenessConstraint>
386+
</uniquenessConstraints>
375387
</entity>
376388
<entity name="ThreadDTO" representedClassName="ThreadDTO" syncable="YES">
377389
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
390+
<attribute name="currentUserUnreadCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
391+
<attribute name="extraData" optional="YES" attributeType="Binary"/>
378392
<attribute name="lastMessageAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
379393
<attribute name="parentMessageId" optional="YES" attributeType="String"/>
380394
<attribute name="participantCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
@@ -434,7 +448,7 @@
434448
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="CurrentUserDTO" inverseName="mutedUsers" inverseEntity="CurrentUserDTO"/>
435449
<relationship name="participatedThreads" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="threadParticipants" inverseEntity="MessageDTO"/>
436450
<relationship name="pinnedMessages" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="pinnedBy" inverseEntity="MessageDTO"/>
437-
<relationship name="pollCreatedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="createdBy" inverseEntity="PollDTO"/>
451+
<relationship name="pollCreatedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="createdBy" inverseEntity="PollDTO"/>
438452
<relationship name="queries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="UserListQueryDTO" inverseName="users" inverseEntity="UserListQueryDTO"/>
439453
<relationship name="reactions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MessageReactionDTO" inverseName="user" inverseEntity="MessageReactionDTO"/>
440454
<relationship name="threadReads" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ThreadReadDTO" inverseName="user" inverseEntity="ThreadReadDTO"/>

StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
@testable import StreamChat
66
@testable import StreamChatSwiftUI
7+
@testable import StreamChatTestTools
78
import XCTest
89

910
class ChatChannelListViewModel_Tests: StreamChatTestCase {
@@ -270,6 +271,59 @@ class ChatChannelListViewModel_Tests: StreamChatTestCase {
270271
// Then
271272
XCTAssert(viewModel.hideTabBar == true)
272273
}
274+
275+
func test_channelListVM_deeplinkToExistingChannel() throws {
276+
// Given
277+
let channels = (0..<3).map { ChatChannel.mock(cid: ChannelId(type: .messaging, id: "\($0)")) }
278+
let channelListController = makeChannelListController(channels: channels)
279+
let selectedId = channels[1].cid
280+
let viewModel = ChatChannelListViewModel(
281+
channelListController: channelListController,
282+
selectedChannelId: selectedId.rawValue
283+
)
284+
285+
// Then
286+
let expectation = XCTestExpectation(description: "SelectedChannel")
287+
let cancellable = viewModel.$selectedChannel
288+
.filter { $0?.channel.cid == selectedId }
289+
.sink { _ in
290+
expectation.fulfill()
291+
}
292+
// Resume synchronize()
293+
chatClient.mockAPIClient.test_simulateResponse(.success(ChannelListPayload(channels: [])))
294+
wait(for: [expectation], timeout: defaultTimeout)
295+
cancellable.cancel()
296+
}
297+
298+
func test_channelListVM_deeplinkToIncomingChannel() {
299+
// Given
300+
let channels = (0..<3).map { ChatChannel.mock(cid: ChannelId(type: .messaging, id: "\($0)")) }
301+
let channelListController = makeChannelListController(channels: channels)
302+
let selectedId = ChannelId(type: .messaging, id: "3")
303+
let viewModel = ChatChannelListViewModel(
304+
channelListController: channelListController,
305+
selectedChannelId: selectedId.rawValue
306+
)
307+
308+
// When
309+
let expectation = XCTestExpectation(description: "SelectedChannel")
310+
let cancellable = viewModel.$selectedChannel
311+
.filter { $0?.channel.cid == selectedId }
312+
.sink { _ in
313+
expectation.fulfill()
314+
}
315+
let insertedChannel = ChatChannel.mock(cid: selectedId)
316+
channelListController.simulate(
317+
channels: channels + [insertedChannel],
318+
changes: [.insert(insertedChannel, index: IndexPath(item: 0, section: 0))]
319+
)
320+
// Resume synchronize()
321+
chatClient.mockAPIClient.test_simulateResponse(.success(ChannelListPayload(channels: [])))
322+
323+
// Then
324+
wait(for: [expectation], timeout: defaultTimeout)
325+
cancellable.cancel()
326+
}
273327

274328
// MARK: - private
275329

0 commit comments

Comments
 (0)