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 @@

- StreamChatSwiftUI + StreamChatSwiftUI

## 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(