diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb8526a5f..847f03589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🔄 Changed
+# [4.87.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.87.0)
+_September 01, 2025_
+
+### ✅ Added
+- Add option to scroll to and open a channel from the channel list [#932](https://github.com/GetStream/stream-chat-swiftui/pull/932)
+- Make `MediaItem` and `MediaAttachmentContentView` public to allow customization [#935](https://github.com/GetStream/stream-chat-swiftui/pull/935)
+
+### 🐞 Fixed
+- Show attachment title instead of URL in the `FileAttachmentPreview` view [#930](https://github.com/GetStream/stream-chat-swiftui/pull/930)
+- Fix overriding title color in `ChannelTitleView` [#931](https://github.com/GetStream/stream-chat-swiftui/pull/931)
+- Use channel capabilities for validating delete message action [#933](https://github.com/GetStream/stream-chat-swiftui/pull/933)
+- Fix the video attachments now fetch the URL once on appear and pause when swiping to another item in the gallery [#934](https://github.com/GetStream/stream-chat-swiftui/pull/934)
+
# [4.86.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.86.0)
_August 21, 2025_
diff --git a/Package.swift b/Package.swift
index 2aafefabc..1d5cba5cc 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.86.0")
+ .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.87.0")
],
targets: [
.target(
diff --git a/README.md b/README.md
index 0c93a569e..c8ecc2b2c 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
## SwiftUI StreamChat SDK
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift
index 2931fb8ad..76add56cb 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift
@@ -145,6 +145,7 @@ public struct ChannelTitleView: View {
VStack(spacing: 2) {
Text(channelNamer(channel, currentUserId) ?? "")
.font(fonts.bodyBold)
+ .foregroundColor(Color(colors.text))
.accessibilityIdentifier("chatName")
if shouldShowTypingIndicator {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift
index 1b5fce921..e92387791 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift
@@ -60,7 +60,7 @@ public struct FileAttachmentsView: View {
.padding(.vertical)
}
.sheet(item: $viewModel.selectedAttachment) { item in
- FileAttachmentPreview(url: item.assetURL)
+ FileAttachmentPreview(title: item.title, url: item.assetURL)
}
Divider()
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
index 449604bf8..39a2a3ada 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
@@ -102,7 +102,7 @@ public struct MediaAttachmentsView: View {
}
}
-struct MediaAttachmentContentView: View {
+public struct MediaAttachmentContentView: View {
@State private var galleryShown = false
let factory: Factory
@@ -112,7 +112,23 @@ struct MediaAttachmentContentView: View {
let itemWidth: CGFloat
let index: Int
- var body: some View {
+ public init(
+ factory: Factory,
+ mediaItem: MediaItem,
+ mediaAttachment: MediaAttachment,
+ allMediaAttachments: [MediaAttachment],
+ itemWidth: CGFloat,
+ index: Int
+ ) {
+ self.factory = factory
+ self.mediaItem = mediaItem
+ self.mediaAttachment = mediaAttachment
+ self.allMediaAttachments = allMediaAttachments
+ self.itemWidth = itemWidth
+ self.index = index
+ }
+
+ public var body: some View {
Button {
galleryShown = true
} label: {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift
index 2b30405fb..af15a7e14 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift
@@ -106,15 +106,29 @@ class MediaAttachmentsViewModel: ObservableObject, ChatMessageSearchControllerDe
}
}
-struct MediaItem: Identifiable {
- let id: String
- let isVideo: Bool
- let message: ChatMessage
+public struct MediaItem: Identifiable {
+ public let id: String
+ public let isVideo: Bool
+ public let message: ChatMessage
- var videoAttachment: ChatMessageVideoAttachment?
- var imageAttachment: ChatMessageImageAttachment?
+ public var videoAttachment: ChatMessageVideoAttachment?
+ public var imageAttachment: ChatMessageImageAttachment?
- var mediaAttachment: MediaAttachment? {
+ public init(
+ id: String,
+ isVideo: Bool,
+ message: ChatMessage,
+ videoAttachment: ChatMessageVideoAttachment?,
+ imageAttachment: ChatMessageImageAttachment?
+ ) {
+ self.id = id
+ self.isVideo = isVideo
+ self.message = message
+ self.videoAttachment = videoAttachment
+ self.imageAttachment = imageAttachment
+ }
+
+ public var mediaAttachment: MediaAttachment? {
if let videoAttachment {
return MediaAttachment(url: videoAttachment.videoURL, type: .video)
} else if let imageAttachment {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
index d8ca25449..68ac63f82 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
@@ -168,6 +168,10 @@ struct StreamVideoPlayer: View {
}
}
.onAppear {
+ guard avPlayer == nil else {
+ avPlayer?.play()
+ return
+ }
fileCDN.adjustedURL(for: url) { result in
switch result {
case let .success(url):
@@ -179,5 +183,8 @@ struct StreamVideoPlayer: View {
}
}
}
+ .onDisappear {
+ avPlayer?.pause()
+ }
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift
index f29ffdfae..cc3b989a0 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift
@@ -16,13 +16,20 @@ public struct FileAttachmentPreview: View {
utils.fileCDN
}
+ let title: String?
var url: URL
@State private var adjustedUrl: URL?
@State private var isLoading = false
- @State private var title: String?
+ @State private var webViewTitle: String?
@State private var error: Error?
-
+
+ var navigationTitle: String {
+ if let title, !title.isEmpty { return title }
+ if let webViewTitle, !webViewTitle.isEmpty { return webViewTitle }
+ return url.absoluteString
+ }
+
public var body: some View {
NavigationView {
ZStack {
@@ -35,7 +42,7 @@ public struct FileAttachmentPreview: View {
WebView(
url: adjustedUrl,
isLoading: $isLoading,
- title: $title,
+ title: $webViewTitle,
error: $error
)
}
@@ -58,7 +65,7 @@ public struct FileAttachmentPreview: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
- Text(title ?? url.absoluteString)
+ Text(navigationTitle)
.font(fonts.bodyBold)
.lineLimit(1)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift
index d8eaece2e..222ce7f9b 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift
@@ -109,7 +109,7 @@ public struct FileAttachmentView: View {
.roundWithBorder()
.withUploadingStateIndicator(for: attachment.uploadingState, url: attachment.assetURL)
.sheet(isPresented: $fullScreenShown) {
- FileAttachmentPreview(url: attachment.assetURL)
+ FileAttachmentPreview(title: attachment.title, url: attachment.assetURL)
}
.accessibilityIdentifier("FileAttachmentView")
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift
index ac5fc515d..8e29a81e9 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift
@@ -156,7 +156,7 @@ public extension MessageAction {
}
}
- if message.isSentByCurrentUser {
+ if channel.canDeleteAnyMessage || channel.canDeleteOwnMessage && message.isSentByCurrentUser {
let deleteAction = deleteMessageAction(
for: message,
channel: channel,
@@ -166,7 +166,9 @@ public extension MessageAction {
)
messageActions.append(deleteAction)
- } else {
+ }
+
+ if !message.isSentByCurrentUser {
if channel.canFlagMessage {
let flagAction = flagMessageAction(
for: message,
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelList.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelList.swift
index 6db822616..5df8b692e 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelList.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelList.swift
@@ -14,6 +14,7 @@ public struct ChannelList: View {
var channels: LazyCachedMapCollection
@Binding var selectedChannel: ChannelSelectionInfo?
@Binding var swipedChannelId: String?
+ @Binding var scrolledChannelId: String?
private var scrollable: Bool
private var onlineIndicatorShown: (ChatChannel) -> Bool
private var imageLoader: (ChatChannel) -> UIImage
@@ -30,6 +31,7 @@ public struct ChannelList: View {
channels: LazyCachedMapCollection,
selectedChannel: Binding,
swipedChannelId: Binding,
+ scrolledChannelId: Binding = .constant(nil),
scrollable: Bool = true,
onlineIndicatorShown: ((ChatChannel) -> Bool)? = nil,
imageLoader: ((ChatChannel) -> UIImage)? = nil,
@@ -72,13 +74,23 @@ public struct ChannelList: View {
self.scrollable = scrollable
_selectedChannel = selectedChannel
_swipedChannelId = swipedChannelId
+ _scrolledChannelId = scrolledChannelId
}
public var body: some View {
Group {
if scrollable {
- ScrollView {
- channelsVStack
+ ScrollViewReader { scrollView in
+ ScrollView {
+ channelsVStack
+ }
+ .onChange(of: scrolledChannelId) { newValue in
+ if let newValue {
+ withAnimation {
+ scrollView.scrollTo(newValue, anchor: .bottom)
+ }
+ }
+ }
}
} else {
channelsVStack
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift
index 155247916..49b1671cb 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift
@@ -234,6 +234,7 @@ public struct ChatChannelListContentView: View {
channels: viewModel.channels,
selectedChannel: $viewModel.selectedChannel,
swipedChannelId: $viewModel.swipedChannelId,
+ scrolledChannelId: $viewModel.scrolledChannelId,
onlineIndicatorShown: viewModel.onlineIndicatorShown(for:),
imageLoader: channelHeaderLoader.image(for:),
onItemTap: onItemTap,
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift
index 04dd1908d..d97b1769e 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift
@@ -39,6 +39,9 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
/// Index of the selected channel.
private var selectedChannelIndex: Int?
+
+ /// When set, scrolls to the specified channel id (if it exists).
+ @Published public var scrolledChannelId: String?
/// Published variables.
@Published public var channels = LazyCachedMapCollection() {
@@ -180,6 +183,38 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
}
}
}
+
+ /// Opens the chat channel destination with the provided channel id.
+ ///
+ /// - Parameter channelId: the id of the channel that will be shown.
+ public func openChannel(with channelId: ChannelId) {
+ func loadUntilFound() {
+ guard let controller else { return }
+ if let channel = controller.channels.first(where: { $0.id == channelId.rawValue }) {
+ log.debug("Showing channel with id \(channelId)")
+ scrollToAndOpen(channel: channel)
+ return
+ }
+
+ // Stop if there are no more channels to load
+ if controller.hasLoadedAllPreviousChannels {
+ scrolledChannelId = nil
+ return
+ }
+
+ controller.loadNextChannels { [weak self] error in
+ if error != nil {
+ self?.scrolledChannelId = nil
+ return
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ loadUntilFound()
+ }
+ }
+ }
+
+ loadUntilFound()
+ }
public func loadAdditionalSearchResults(index: Int) {
switch searchType {
@@ -543,6 +578,14 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
markDirty = true
channels = LazyCachedMapCollection(source: temp, map: { $0 })
}
+
+ private func scrollToAndOpen(channel: ChatChannel) {
+ scrolledChannelId = channel.id
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
+ self?.selectedChannel = .init(channel: channel, message: nil)
+ self?.scrolledChannelId = nil
+ }
+ }
private func observeChannelDismiss() {
NotificationCenter.default.addObserver(
diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
index 549d7671e..27d20863c 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.86.0"
+ public static let version: String = "4.87.0"
}
diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist
index 21853bfa2..93c8c9227 100644
--- a/Sources/StreamChatSwiftUI/Info.plist
+++ b/Sources/StreamChatSwiftUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.86.0
+ 4.87.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPhotoLibraryUsageDescription
diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec
index b78dad513..3c37a188b 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.86.0'
+ spec.version = '4.87.0'
spec.summary = 'StreamChat SwiftUI Chat Components'
spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.'
@@ -19,7 +19,7 @@ Pod::Spec.new do |spec|
spec.framework = 'Foundation', 'UIKit', 'SwiftUI'
- spec.dependency 'StreamChat-XCFramework', '~> 4.86.0'
+ spec.dependency 'StreamChat-XCFramework', '~> 4.87.0'
spec.cocoapods_version = '>= 1.11.0'
end
diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec
index de7f95e9e..005f3f361 100644
--- a/StreamChatSwiftUI.podspec
+++ b/StreamChatSwiftUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamChatSwiftUI'
- spec.version = '4.86.0'
+ spec.version = '4.87.0'
spec.summary = 'StreamChat SwiftUI Chat Components'
spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.'
@@ -19,5 +19,5 @@ Pod::Spec.new do |spec|
spec.framework = 'Foundation', 'UIKit', 'SwiftUI'
- spec.dependency 'StreamChat', '~> 4.86.0'
+ spec.dependency 'StreamChat', '~> 4.87.0'
end
diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj
index 169af8f8f..fbe53bc56 100644
--- a/StreamChatSwiftUI.xcodeproj/project.pbxproj
+++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj
@@ -32,6 +32,7 @@
4FD3592A2C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD359292C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift */; };
4FD964622D353D88001B6838 /* FilePickerView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD964612D353D82001B6838 /* FilePickerView_Tests.swift */; };
4FEAB3182BFF71F70057E511 /* SwiftUI+UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */; };
+ 4FEDF72B2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEDF72A2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift */; };
8205B4142AD41CC700265B84 /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8205B4132AD41CC700265B84 /* StreamSwiftTestHelpers */; };
8205B4182AD4267200265B84 /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8205B4172AD4267200265B84 /* StreamSwiftTestHelpers */; };
820A61A029D6D78E002257FB /* QuotedReply_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820A619F29D6D78E002257FB /* QuotedReply_Tests.swift */; };
@@ -635,6 +636,7 @@
4FD359292C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollViewModel_Tests.swift; sourceTree = ""; };
4FD964612D353D82001B6838 /* FilePickerView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePickerView_Tests.swift; sourceTree = ""; };
4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+UIAlertController.swift"; sourceTree = ""; };
+ 4FEDF72A2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttachmentPreview_Tests.swift; sourceTree = ""; };
820A619F29D6D78E002257FB /* QuotedReply_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReply_Tests.swift; sourceTree = ""; };
825AADF3283CCDB000237498 /* ThreadPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPage.swift; sourceTree = ""; };
829AB4D128578ACF002DC629 /* StreamTestCase+Tags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StreamTestCase+Tags.swift"; sourceTree = ""; };
@@ -2001,6 +2003,7 @@
84B2B5D528196FD100479CEE /* MediaAttachmentsView_Tests.swift */,
84B2B5D72819778D00479CEE /* FileAttachmentsViewModel_Tests.swift */,
84B2B5D9281985DA00479CEE /* FileAttachmentsView_Tests.swift */,
+ 4FEDF72A2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift */,
84507C97281AC40F0081DDC2 /* AddUsersViewModel_Tests.swift */,
84507C99281ACCD70081DDC2 /* AddUsersView_Tests.swift */,
);
@@ -3076,6 +3079,7 @@
84C94D1327578BF2007FE2B9 /* XCTestCase+MockJSON.swift in Sources */,
84C94D5E275A3AA9007FE2B9 /* ImageCDN_Tests.swift in Sources */,
84C94C8027567D3F007FE2B9 /* ChatChannelListViewModel_Tests.swift in Sources */,
+ 4FEDF72B2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift in Sources */,
84B9B20E27998E9200BFAEAE /* ColorExtensions.swift in Sources */,
8423C344277CC5020092DCF1 /* CommandsHandler_Tests.swift in Sources */,
84B2B5D2281965D000479CEE /* MediaAttachmentsViewModel_Tests.swift in Sources */,
@@ -3908,7 +3912,7 @@
repositoryURL = "https://github.com/GetStream/stream-chat-swift.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 4.86.0;
+ minimumVersion = 4.87.0;
};
};
E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json
index cfd95bd1c..17587a6f3 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"}
\ 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"}
\ No newline at end of file
diff --git a/StreamChatSwiftUITests/Infrastructure/Mocks/CDNClient_Mock.swift b/StreamChatSwiftUITests/Infrastructure/Mocks/CDNClient_Mock.swift
index a1d405630..37c1cbdd2 100644
--- a/StreamChatSwiftUITests/Infrastructure/Mocks/CDNClient_Mock.swift
+++ b/StreamChatSwiftUITests/Infrastructure/Mocks/CDNClient_Mock.swift
@@ -22,4 +22,10 @@ final class CDNClient_Mock: CDNClient {
)
)
}
+
+ func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ ) {}
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/FileAttachmentPreview_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/FileAttachmentPreview_Tests.swift
new file mode 100644
index 000000000..d1b7c997c
--- /dev/null
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/FileAttachmentPreview_Tests.swift
@@ -0,0 +1,20 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import SnapshotTesting
+@testable import StreamChatSwiftUI
+@testable import StreamChatTestTools
+import SwiftUI
+import XCTest
+
+class FileAttachmentPreview_Tests: StreamChatTestCase {
+ func test_fileAttachmentPreview_pdf() {
+ let view = FileAttachmentPreview(
+ title: "Document title",
+ url: URL.localYodaQuote
+ ).applyDefaultSize()
+
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/__Snapshots__/FileAttachmentPreview_Tests/test_fileAttachmentPreview_pdf.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/__Snapshots__/FileAttachmentPreview_Tests/test_fileAttachmentPreview_pdf.1.png
new file mode 100644
index 000000000..30822789e
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/__Snapshots__/FileAttachmentPreview_Tests/test_fileAttachmentPreview_pdf.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelHeader_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelHeader_Tests.swift
index 5e768b440..005582158 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelHeader_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelHeader_Tests.swift
@@ -76,4 +76,21 @@ class ChatChannelHeader_Tests: StreamChatTestCase {
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}
+
+ func test_channelTitleView_theme_snapshot() {
+ // Given
+ let channel = ChatChannel.mockDMChannel(name: "Test channel")
+
+ // When
+ adjustAppearance { appearance in
+ appearance.colors.text = .red
+ appearance.colors.subtitleText = .blue
+ }
+ let size = CGSize(width: 300, height: 100)
+ let view = ChannelTitleView(channel: channel, shouldShowTypingIndicator: true)
+ .applySize(size)
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift
index b137dec2c..f693428d1 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift
@@ -374,12 +374,45 @@ class MessageActions_Tests: StreamChatTestCase {
// Then
XCTAssertTrue(messageActions.contains(where: { $0.title == "Edit Message" }))
}
+
+ func test_messageActions_otherUser_deletingEnabledWhenDeleteAnyMessageCapability() {
+ // Given
+ let channel = ChatChannel.mockDMChannel(ownCapabilities: [.deleteAnyMessage])
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: channel.cid,
+ text: "Test",
+ author: .mock(id: .unique),
+ isSentByCurrentUser: false
+ )
+ let factory = DefaultViewFactory.shared
+
+ // When
+ let messageActions = MessageAction.defaultActions(
+ factory: factory,
+ for: message,
+ channel: channel,
+ chatClient: chatClient,
+ onFinish: { _ in },
+ onError: { _ in }
+ )
+
+ // Then
+ XCTAssertTrue(messageActions.contains(where: { $0.title == "Delete Message" }))
+ }
// MARK: - Private
private var mockDMChannel: ChatChannel {
ChatChannel.mockDMChannel(
- ownCapabilities: [.updateOwnMessage, .sendMessage, .uploadFile, .pinMessage, .readEvents]
+ ownCapabilities: [
+ .deleteOwnMessage,
+ .updateOwnMessage,
+ .sendMessage,
+ .uploadFile,
+ .pinMessage,
+ .readEvents
+ ]
)
}
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelHeader_Tests/test_channelTitleView_theme_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelHeader_Tests/test_channelTitleView_theme_snapshot.1.png
new file mode 100644
index 000000000..6c198bafe
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelHeader_Tests/test_channelTitleView_theme_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift
index 8e8d0ab02..2082d3731 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift
@@ -372,6 +372,126 @@ class ChatChannelListViewModel_Tests: StreamChatTestCase {
XCTAssertNotNil(viewModel.messageSearchController)
}
+ // MARK: - Open Channel
+
+ func test_openChannel_whenChannelExistsInList_shouldScrollToAndOpenChannel() {
+ // Given
+ let channel = ChatChannel.mockDMChannel()
+ let channelListController = makeChannelListController(channels: [channel])
+ let viewModel = ChatChannelListViewModel(
+ channelListController: channelListController,
+ selectedChannelId: nil
+ )
+
+ // When
+ viewModel.openChannel(with: channel.cid)
+
+ // Then
+ XCTAssertEqual(viewModel.scrolledChannelId, channel.id)
+
+ // Wait for the async delay and verify selectedChannel is set
+ let expectation = XCTestExpectation(description: "Channel opened")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ XCTAssertEqual(viewModel.selectedChannel?.channel.id, channel.id)
+ XCTAssertNil(viewModel.scrolledChannelId)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func test_openChannel_whenChannelNotInList_shouldLoadNextChannelsUntilFound() {
+ // Given
+ let existingChannel = ChatChannel.mockDMChannel()
+ let targetChannel = ChatChannel.mockDMChannel()
+ let channelListController = makeChannelListController(channels: [existingChannel])
+ let viewModel = ChatChannelListViewModel(
+ channelListController: channelListController,
+ selectedChannelId: nil
+ )
+
+ // When
+ viewModel.openChannel(with: targetChannel.cid)
+
+ // Then
+ XCTAssertEqual(channelListController.loadNextChannelsCallCount, 1)
+
+ // Simulate the channel being found after loading
+ channelListController.simulate(
+ channels: [existingChannel, targetChannel],
+ changes: [.insert(targetChannel, index: .init(row: 1, section: 0))]
+ )
+
+ // When
+ viewModel.openChannel(with: targetChannel.cid)
+
+ // Verify the channel is eventually opened
+ let expectation = XCTestExpectation(description: "Channel opened after loading")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ XCTAssertEqual(viewModel.selectedChannel?.channel.id, targetChannel.id)
+ XCTAssertNil(viewModel.scrolledChannelId)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func test_openChannel_whenChannelNotFoundAndNoMoreChannels_shouldSetScrolledChannelIdToNil() {
+ // Given
+ let existingChannel = ChatChannel.mockDMChannel()
+ let targetChannel = ChatChannel.mockDMChannel()
+ let channelListController = makeChannelListController(channels: [existingChannel])
+ let viewModel = ChatChannelListViewModel(
+ channelListController: channelListController,
+ selectedChannelId: nil
+ )
+
+ // When
+ viewModel.openChannel(with: targetChannel.cid)
+
+ // Then
+ XCTAssertNil(viewModel.scrolledChannelId)
+ XCTAssertNil(viewModel.selectedChannel)
+ }
+
+ func test_openChannel_whenChannelFoundAfterMultipleLoads_shouldEventuallyOpenChannel() {
+ // Given
+ let existingChannel = ChatChannel.mockDMChannel()
+ let targetChannel = ChatChannel.mockDMChannel()
+ let channelListController = makeChannelListController(channels: [existingChannel])
+ let viewModel = ChatChannelListViewModel(
+ channelListController: channelListController,
+ selectedChannelId: nil
+ )
+
+ // When
+ viewModel.openChannel(with: targetChannel.cid)
+
+ // Then
+ XCTAssertEqual(channelListController.loadNextChannelsCallCount, 1)
+
+ // Simulate first load not finding the channel
+ channelListController.simulate(
+ channels: [existingChannel],
+ changes: []
+ )
+
+ // Simulate second load finding the channel
+ channelListController.simulate(
+ channels: [existingChannel, targetChannel],
+ changes: [.insert(targetChannel, index: .init(row: 1, section: 0))]
+ )
+
+ viewModel.openChannel(with: targetChannel.cid)
+
+ // Verify the channel is eventually opened
+ let expectation = XCTestExpectation(description: "Channel opened after multiple loads")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ XCTAssertEqual(viewModel.selectedChannel?.channel.id, targetChannel.id)
+ XCTAssertNil(viewModel.scrolledChannelId)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1.0)
+ }
+
// MARK: - private
private func makeChannelListController(