Skip to content

Commit

Permalink
Channel was sometimes not selected when setting selectedChannelId (#611)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Martin Mitrevski <[email protected]>
  • Loading branch information
laevandus and martinmitrevski authored Oct 3, 2024
1 parent 20a2d4a commit daa05d6
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Combine
import Foundation
import StreamChat
import SwiftUI
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23C71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AttachmentDTO" representedClassName="AttachmentDTO" syncable="YES">
<attribute name="data" attributeType="Binary"/>
<attribute name="id" attributeType="String"/>
<attribute name="localDownloadStateRaw" optional="YES" attributeType="String"/>
<attribute name="localProgress" attributeType="Double" minValueString="0" maxValueString="1" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="localRelativePath" optional="YES" attributeType="String"/>
<attribute name="localStateRaw" optional="YES" attributeType="String"/>
<attribute name="localURL" optional="YES" attributeType="URI"/>
<attribute name="type" optional="YES" attributeType="String"/>
Expand All @@ -20,6 +22,7 @@
<attribute name="maxMessageLength" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messageRetention" attributeType="String" defaultValueString=""/>
<attribute name="mutesEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="pollsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="quotesEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="reactionsEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="readEventsEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
Expand All @@ -36,10 +39,12 @@
<attribute name="cid" attributeType="String"/>
<attribute name="cooldownDuration" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="currentUserUnreadMessagesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="defaultSortingAt" attributeType="Date" usesScalarValueType="NO" spotlightIndexingEnabled="YES"/>
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="extraData" attributeType="Binary"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isBlocked" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isFrozen" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isHidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastMessageAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
Expand Down Expand Up @@ -132,13 +137,15 @@
<relationship name="channelConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ChannelConfigDTO" inverseName="commands" inverseEntity="ChannelConfigDTO"/>
</entity>
<entity name="CurrentUserDTO" representedClassName="CurrentUserDTO" syncable="YES">
<attribute name="blockedUserIds" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer"/>
<attribute name="isInvisible" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isReadReceiptsEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isTypingIndicatorsEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="lastSynchedEventDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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"/>
<attribute name="unreadChannelsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="unreadMessagesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="unreadThreadsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channelMutes" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ChannelMuteDTO" inverseName="currentUser" inverseEntity="ChannelMuteDTO"/>
<relationship name="currentDevice" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceDTO" inverseName="relationship" inverseEntity="DeviceDTO"/>
<relationship name="devices" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="DeviceDTO" inverseName="user" inverseEntity="DeviceDTO"/>
Expand Down Expand Up @@ -323,10 +330,10 @@
<attribute name="voteCountsByOption" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData"/>
<attribute name="votingVisibility" optional="YES" attributeType="String"/>
<relationship name="createdBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserDTO" inverseName="pollCreatedBy" inverseEntity="UserDTO"/>
<relationship name="latestAnswers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteDTO" inverseName="poll" inverseEntity="PollVoteDTO"/>
<relationship name="latestVotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteDTO" inverseName="poll" inverseEntity="PollVoteDTO"/>
<relationship name="latestVotesByOption" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="pollLatestVotes" inverseEntity="PollOptionDTO"/>
<relationship name="message" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="poll" inverseEntity="MessageDTO"/>
<relationship name="options" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="poll" inverseEntity="PollOptionDTO"/>
<relationship name="options" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PollOptionDTO" inverseName="poll" inverseEntity="PollOptionDTO"/>
</entity>
<entity name="PollOptionDTO" representedClassName="PollOptionDTO" syncable="YES">
<attribute name="custom" optional="YES" attributeType="Binary"/>
Expand All @@ -345,7 +352,7 @@
<attribute name="pollId" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="option" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="latestVotes" inverseEntity="PollOptionDTO"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="latestAnswers" inverseEntity="PollDTO"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="latestVotes" inverseEntity="PollDTO"/>
<relationship name="queries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteListQueryDTO" inverseName="votes" inverseEntity="PollVoteListQueryDTO"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserDTO" inverseName="votes" inverseEntity="UserDTO"/>
</entity>
Expand All @@ -372,9 +379,16 @@
<fetchIndex name="filterHash">
<fetchIndexElement property="filterHash" type="Binary" order="ascending"/>
</fetchIndex>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="filterHash"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ThreadDTO" representedClassName="ThreadDTO" syncable="YES">
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="currentUserUnreadCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="extraData" optional="YES" attributeType="Binary"/>
<attribute name="lastMessageAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="parentMessageId" optional="YES" attributeType="String"/>
<attribute name="participantCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
Expand Down Expand Up @@ -434,7 +448,7 @@
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="CurrentUserDTO" inverseName="mutedUsers" inverseEntity="CurrentUserDTO"/>
<relationship name="participatedThreads" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="threadParticipants" inverseEntity="MessageDTO"/>
<relationship name="pinnedMessages" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="pinnedBy" inverseEntity="MessageDTO"/>
<relationship name="pollCreatedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="createdBy" inverseEntity="PollDTO"/>
<relationship name="pollCreatedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="createdBy" inverseEntity="PollDTO"/>
<relationship name="queries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="UserListQueryDTO" inverseName="users" inverseEntity="UserListQueryDTO"/>
<relationship name="reactions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MessageReactionDTO" inverseName="user" inverseEntity="MessageReactionDTO"/>
<relationship name="threadReads" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ThreadReadDTO" inverseName="user" inverseEntity="ThreadReadDTO"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

@testable import StreamChat
@testable import StreamChatSwiftUI
@testable import StreamChatTestTools
import XCTest

class ChatChannelListViewModel_Tests: StreamChatTestCase {
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit daa05d6

Please sign in to comment.