diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml new file mode 100644 index 00000000..dcdb50d7 --- /dev/null +++ b/.github/workflows/testflight.yml @@ -0,0 +1,44 @@ +name: Test Flight Deploy DemoApp + +on: + pull_request: + branches: + - 'main' + + release: + types: [published] + + workflow_dispatch: + +env: + HOMEBREW_NO_INSTALL_CLEANUP: 1 + +jobs: + deploy: + runs-on: macos-14 + steps: + - name: Connect Bot + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + - uses: ./.github/actions/ruby-cache + - uses: ./.github/actions/xcode-cache + - name: Deploy Demo app + env: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.number }} + run: bundle exec fastlane swiftui_testflight_build + - uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + text: "You shall not pass!" + fields: message,commit,author,action,workflow,job,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + MATRIX_CONTEXT: ${{ toJson(matrix) }} + if: failure() diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8abaa9..2df8c568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🔄 Changed +# [4.66.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.66.0) +_November 06, 2024_ + +### ✅ Added +- Add support for Channel Search in the Channel List [#628](https://github.com/GetStream/stream-chat-swiftui/pull/628) +### 🐞 Fixed +- Fix crash when opening message overlay in iPad with a TabBar [#627](https://github.com/GetStream/stream-chat-swiftui/pull/627) +- Only show Leave Group option if the user has leave-channel permission [#633](https://github.com/GetStream/stream-chat-swiftui/pull/633) +- Fix Channel List stuck in Empty View State in rare conditions [#639](https://github.com/GetStream/stream-chat-swiftui/pull/639) +- Fix a bug with photo attachment picker indicator not displaying [#640](https://github.com/GetStream/stream-chat-swiftui/pull/640) + # [4.65.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.65.0) _October 18, 2024_ diff --git a/DemoAppSwiftUI/DemoAppSwiftUIApp.swift b/DemoAppSwiftUI/DemoAppSwiftUIApp.swift index 87c6ea6a..4f3927c7 100644 --- a/DemoAppSwiftUI/DemoAppSwiftUIApp.swift +++ b/DemoAppSwiftUI/DemoAppSwiftUIApp.swift @@ -18,7 +18,11 @@ struct DemoAppSwiftUIApp: App { var channelListController: ChatChannelListController? { appState.channelListController } - + + var channelListSearchType: ChannelListSearchType { + .messages + } + var body: some Scene { WindowGroup { switch appState.userState { @@ -64,12 +68,14 @@ struct DemoAppSwiftUIApp: App { ChatChannelListView( viewFactory: DemoAppFactory.shared, channelListController: channelListController, - selectedChannelId: notificationsHandler.notificationChannelId + selectedChannelId: notificationsHandler.notificationChannelId, + searchType: channelListSearchType ) } else { ChatChannelListView( viewFactory: DemoAppFactory.shared, - channelListController: channelListController + channelListController: channelListController, + searchType: channelListSearchType ) } } diff --git a/DemoAppSwiftUI/DemoUser.swift b/DemoAppSwiftUI/DemoUser.swift index 54a62cb8..12d53b5a 100644 --- a/DemoAppSwiftUI/DemoUser.swift +++ b/DemoAppSwiftUI/DemoUser.swift @@ -11,9 +11,23 @@ public let currentUserIdRegisteredForPush = "currentUserIdRegisteredForPush" public struct UserCredentials: Codable { public let id: String public let name: String - public let avatarURL: URL + public let avatarURL: URL? public let token: String public let birthLand: String + + var isGuest: Bool { + id == "guest" + } + + static var guestUser: UserCredentials { + UserCredentials( + id: "guest", + name: "Guest", + avatarURL: nil, + token: "", + birthLand: "" + ) + } } extension UserCredentials: Identifiable { @@ -135,8 +149,7 @@ extension UserCredentials: Identifiable { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZ2VuZXJhbF9ncmlldm91cyJ9.g2UUZdENuacFIxhYCylBuDJZUZ2x59MTWaSpndWGCTU", "Qymaen jai Sheelal" ) - ].map { UserCredentials(id: $0.0, name: $0.1, avatarURL: URL(string: $0.2)!, token: $0.3, birthLand: $0.4) - } + } + [UserCredentials.guestUser] } diff --git a/DemoAppSwiftUI/LoginView.swift b/DemoAppSwiftUI/LoginView.swift index 9a5c41f1..a2ebf836 100644 --- a/DemoAppSwiftUI/LoginView.swift +++ b/DemoAppSwiftUI/LoginView.swift @@ -56,15 +56,25 @@ struct DemoUserView: View { var body: some View { HStack { - StreamLazyImage( - url: user.avatarURL, - size: CGSize(width: imageSize, height: imageSize) - ) + if user.isGuest { + Image(systemName: "person.fill") + .resizable() + .foregroundColor(colors.tintColor) + .frame(width: imageSize, height: imageSize) + .aspectRatio(contentMode: .fit) + .background(Color(colors.background6)) + .clipShape(Circle()) + } else { + StreamLazyImage( + url: user.avatarURL, + size: CGSize(width: imageSize, height: imageSize) + ) + } VStack(alignment: .leading, spacing: 4) { Text(user.name) .font(fonts.bodyBold) - Text("Stream test account") + Text(user.isGuest ? "Login as Guest" : "Stream test account") .font(fonts.footnote) .foregroundColor(Color(colors.textLowEmphasis)) } diff --git a/DemoAppSwiftUI/LoginViewModel.swift b/DemoAppSwiftUI/LoginViewModel.swift index 673dec32..e6907017 100644 --- a/DemoAppSwiftUI/LoginViewModel.swift +++ b/DemoAppSwiftUI/LoginViewModel.swift @@ -14,6 +14,11 @@ class LoginViewModel: ObservableObject { @Injected(\.chatClient) var chatClient func demoUserTapped(_ user: UserCredentials) { + if user.isGuest { + connectGuestUser(withCredentials: user) + return + } + connectUser(withCredentials: user) } @@ -25,7 +30,7 @@ class LoginViewModel: ObservableObject { chatClient.connectUser( userInfo: .init(id: credentials.id, name: credentials.name, imageURL: credentials.avatarURL), token: token - ) { error in + ) { [weak self] error in if let error = error { log.error("connecting the user failed \(error)") return @@ -40,4 +45,25 @@ class LoginViewModel: ObservableObject { } } } + + private func connectGuestUser(withCredentials credentials: UserCredentials) { + loading = true + LogConfig.level = .warning + + chatClient.connectGuestUser( + userInfo: .init(id: credentials.id, name: credentials.name) + ) { [weak self] error in + if let error = error { + log.error("connecting the user failed \(error)") + return + } + + DispatchQueue.main.async { [weak self] in + withAnimation { + self?.loading = false + AppState.shared.userState = .loggedIn + } + } + } + } } diff --git a/Gemfile.lock b/Gemfile.lock index c3396ecf..35964476 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,7 +200,7 @@ GEM fastlane pry fastlane-plugin-sonarcloud_metric_kit (0.2.1) - fastlane-plugin-stream_actions (0.3.70) + fastlane-plugin-stream_actions (0.3.71) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.6.0) ffi (1.17.0) @@ -329,7 +329,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.8) + rexml (3.3.9) rouge (2.0.7) rubocop (1.38.0) json (~> 2.3) @@ -427,7 +427,7 @@ DEPENDENCIES fastlane-plugin-create_xcframework fastlane-plugin-lizard fastlane-plugin-sonarcloud_metric_kit - fastlane-plugin-stream_actions (= 0.3.70) + fastlane-plugin-stream_actions (= 0.3.71) fastlane-plugin-versioning jazzy json diff --git a/Package.swift b/Package.swift index 98afbd12..d61121b0 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.65.0"), + .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.66.0"), ], targets: [ .target( diff --git a/README.md b/README.md index e94b1178..abf60a16 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- StreamChatSwiftUI + StreamChatSwiftUI

## SwiftUI StreamChat SDK diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoViewModel.swift index b3deb4d6..7e3bd71c 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoViewModel.swift @@ -37,8 +37,11 @@ public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDe @Published public var addUsersShown = false public var shouldShowLeaveConversationButton: Bool { - channel.ownCapabilities.contains(.deleteChannel) - || !channel.isDirectMessageChannel + if channel.isDirectMessageChannel { + return channel.ownCapabilities.contains(.deleteChannel) + } else { + return channel.ownCapabilities.contains(.leaveChannel) + } } public var canRenameChannel: Bool { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/PhotoAttachmentPickerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/PhotoAttachmentPickerView.swift index a95f50ef..aaab8572 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/PhotoAttachmentPickerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/PhotoAttachmentPickerView.swift @@ -58,6 +58,7 @@ public struct PhotoAttachmentCell: View { @State private var compressing = false @State private var loading = false @State var requestId: PHContentEditingInputRequestID? + @State var idOverlay = UUID() var asset: PHAsset var onImageTap: (AddedAsset) -> Void @@ -113,6 +114,7 @@ public struct PhotoAttachmentCell: View { ) ) } + idOverlay = UUID() } } } @@ -150,6 +152,7 @@ public struct PhotoAttachmentCell: View { ) } } + .id(idOverlay) ) .onAppear { self.loading = false diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift index 4c0ade86..633fc626 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift @@ -153,20 +153,40 @@ public struct GalleryView: View { } struct StreamVideoPlayer: View { - - @State var player: AVPlayer - + + @Injected(\.utils) private var utils + + private var fileCDN: FileCDN { + utils.fileCDN + } + + let url: URL + + @State var avPlayer: AVPlayer? + @State var error: Error? + init(url: URL) { - let player = AVPlayer(url: url) - _player = State(wrappedValue: player) + self.url = url } var body: some View { - VideoPlayer(player: player) - .clipped() - .onAppear { - try? AVAudioSession.sharedInstance().setCategory(.playback, options: []) - player.play() + VStack { + if let avPlayer { + VideoPlayer(player: avPlayer) + .clipped() + } + } + .onAppear { + fileCDN.adjustedURL(for: url) { result in + switch result { + case let .success(url): + self.avPlayer = AVPlayer(url: url) + try? AVAudioSession.sharedInstance().setCategory(.playback, options: []) + self.avPlayer?.play() + case let .failure(error): + self.error = error + } } + } } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift index f1a7bd13..665de463 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift @@ -12,12 +12,18 @@ public struct VideoPlayerView: View { @Injected(\.fonts) private var fonts @Injected(\.colors) private var colors + @Injected(\.utils) private var utils + + private var fileCDN: FileCDN { + utils.fileCDN + } let attachment: ChatMessageVideoAttachment let author: ChatUser @Binding var isShown: Bool - private let avPlayer: AVPlayer + @State private var avPlayer: AVPlayer? + @State private var error: Error? public init( attachment: ChatMessageVideoAttachment, @@ -26,7 +32,6 @@ public struct VideoPlayerView: View { ) { self.attachment = attachment self.author = author - avPlayer = AVPlayer(url: attachment.payload.videoURL) _isShown = isShown } @@ -37,7 +42,9 @@ public struct VideoPlayerView: View { subtitle: author.onlineText, isShown: $isShown ) - VideoPlayer(player: avPlayer) + if let avPlayer { + VideoPlayer(player: avPlayer) + } Spacer() HStack { ShareButtonView(content: [attachment.payload.videoURL]) @@ -48,11 +55,19 @@ public struct VideoPlayerView: View { .foregroundColor(Color(colors.text)) } .onAppear { - try? AVAudioSession.sharedInstance().setCategory(.playback, options: []) - avPlayer.play() + fileCDN.adjustedURL(for: attachment.payload.videoURL) { result in + switch result { + case let .success(url): + self.avPlayer = AVPlayer(url: url) + try? AVAudioSession.sharedInstance().setCategory(.playback, options: []) + self.avPlayer?.play() + case let .failure(error): + self.error = error + } + } } .onDisappear { - avPlayer.replaceCurrentItem(with: nil) + avPlayer?.replaceCurrentItem(with: nil) } } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift index 8cef1050..0ccb4b58 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift @@ -18,7 +18,7 @@ public struct FileAttachmentPreview: View { var url: URL - @State var adjustedUrl: URL? + @State private var adjustedUrl: URL? @State private var isLoading = false @State private var title: String? @State private var error: Error? diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift index 28f7f424..1741728e 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift @@ -71,7 +71,7 @@ public struct ReactionsOverlayView: View { currentSnapshot: currentSnapshot, popInAnimationInProgress: !popIn ) - .offset(y: spacing > 0 ? screenHeight - currentSnapshot.size.height : 0) + .offset(y: overlayOffsetY) } else { Color.gray.opacity(0.4) } @@ -290,7 +290,16 @@ public struct ReactionsOverlayView: View { return originY - spacing } - + + private var overlayOffsetY: CGFloat { + if isIPad && UITabBar.appearance().isHidden == false { + // When using iPad with TabBar, this hard coded value makes + // sure that the overlay is in the correct position. + return 20 + } + return spacing > 0 ? screenHeight - currentSnapshot.size.height : 0 + } + private var spacing: CGFloat { let divider: CGFloat = isIPad ? 2 : 1 let spacing = (UIScreen.main.bounds.height - screenHeight) / divider diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelHelpers.swift b/Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelHelpers.swift index ee9fcec6..e03704ce 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelHelpers.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelHelpers.swift @@ -82,6 +82,9 @@ public struct BottomLeftView: View { /// Returns the top most view controller. func topVC() -> UIViewController? { + // TODO: Refactor ReactionsOverlayView to use a background blur, instead of a snapshot. + /// Since the current approach is too error-prone and dependent of the app's hierarchy, + let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first if var topController = keyWindow?.rootViewController { @@ -92,10 +95,16 @@ func topVC() -> UIViewController? { if UIDevice.current.userInterfaceIdiom == .pad { let children = topController.children if !children.isEmpty { - let splitVC = children[0] - let sideVCs = splitVC.children - if sideVCs.count > 1 { - topController = sideVCs[1] + if let splitVC = children[0] as? UISplitViewController, + let contentVC = splitVC.viewControllers.last { + topController = contentVC + return topController + } else if let tabVC = children[0] as? UITabBarController, + let selectedVC = tabVC.selectedViewController { + // If the selectedVC is split view, we need to grab the content view of it + // other wise, the selectedVC is already the content view. + let selectedContentVC = selectedVC.children.first?.children.last?.children.first + topController = selectedContentVC ?? selectedVC return topController } } diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChannelHeaderLoader.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChannelHeaderLoader.swift index b0cf7fdc..93a1b6ef 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChannelHeaderLoader.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChannelHeaderLoader.swift @@ -82,7 +82,7 @@ open class ChannelHeaderLoader: ObservableObject { } } - func channelAvatarChanged(_ cid: ChannelId?) -> AnyPublisher { + open func channelAvatarChanged(_ cid: ChannelId?) -> AnyPublisher { didLoadImage .filter { $0 == cid } .map { _ in () } diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift index 497cce77..d340e49c 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift @@ -19,7 +19,7 @@ public struct ChatChannelListView: View { private let customOnItemTap: ((ChatChannel) -> Void)? private var embedInNavigationView: Bool private var handleTabBarVisibility: Bool - + /// Creates a channel list view. /// /// - Parameters: @@ -31,6 +31,7 @@ public struct ChatChannelListView: View { /// - selectedChannelId: The id of a channel to be opened after the initial channel list load. /// - handleTabBarVisibility: True, if TabBar visibility should be automatically updated. /// - embedInNavigationView: True, if the channel list view should be embedded in a navigation stack. + /// - searchType: The type of data the channel list should perform a search. By default it searches messages. /// /// Changing the instance of the passed in `viewModel` or `channelListController` does not have an effect without reloading the channel list view by assigning a custom identity. The custom identity should be refreshed when either of the passed in instances have been recreated. /// ```swift @@ -47,12 +48,14 @@ public struct ChatChannelListView: View { onItemTap: ((ChatChannel) -> Void)? = nil, selectedChannelId: String? = nil, handleTabBarVisibility: Bool = true, - embedInNavigationView: Bool = true + embedInNavigationView: Bool = true, + searchType: ChannelListSearchType = .messages ) { _viewModel = StateObject( wrappedValue: viewModel ?? ViewModelsFactory.makeChannelListViewModel( channelListController: channelListController, - selectedChannelId: selectedChannelId + selectedChannelId: selectedChannelId, + searchType: searchType ) ) self.viewFactory = viewFactory diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift index 1fe060e3..3ac38037 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift @@ -30,8 +30,6 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController /// Temporarly holding changes while message list is shown. private var queuedChannelsChanges = LazyCachedMapCollection() - private var messageSearchController: ChatMessageSearchController? - private var timer: Timer? /// Controls loading the channels. @@ -103,6 +101,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController } } + private let searchType: ChannelListSearchType + internal var channelListSearchController: ChatChannelListController? + internal var messageSearchController: ChatMessageSearchController? + + @Published public var loadingSearchResults = false + @Published public var searchResults = [ChannelSelectionInfo]() + @Published var hideTabBar = false @Published public var searchText = "" { didSet { if searchText != oldValue { @@ -111,10 +116,6 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController } } - @Published public var loadingSearchResults = false - @Published public var searchResults = [ChannelSelectionInfo]() - @Published var hideTabBar = false - public var isSearching: Bool { !searchText.isEmpty } @@ -125,10 +126,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController /// - 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. + /// - searchType: The type of data the channel list should perform a search. public init( channelListController: ChatChannelListController? = nil, - selectedChannelId: String? = nil + selectedChannelId: String? = nil, + searchType: ChannelListSearchType = .channels ) { + self.searchType = searchType self.selectedChannelId = selectedChannelId if let channelListController = channelListController { controller = channelListController @@ -168,21 +172,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController } public func loadAdditionalSearchResults(index: Int) { - guard let messageSearchController = messageSearchController else { - return - } - - if index < messageSearchController.messages.count - 10 { - return - } - - if !loadingNextChannels { - loadingNextChannels = true - messageSearchController.loadNextMessages { [weak self] _ in - guard let self = self else { return } - self.loadingNextChannels = false - self.updateSearchResults() - } + switch searchType { + case .channels: + loadAdditionalChannelSearchResults(index: index) + case .messages: + loadAdditionalMessageSearchResults(index: index) + default: + break } } @@ -258,7 +254,7 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController // MARK: - ChatMessageSearchControllerDelegate public func controller(_ controller: ChatMessageSearchController, didChangeMessages changes: [ListChange]) { - updateSearchResults() + updateMessageSearchResults() } // MARK: - private @@ -314,9 +310,7 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController updateChannels() - if channels.isEmpty { - loading = networkReachability.isNetworkAvailable() - } + loading = channels.isEmpty controller?.synchronize { [weak self] error in guard let self = self else { return } @@ -326,9 +320,7 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController self.channelAlertType = .error } else { // access channels - if self.selectedChannel == nil { - self.updateChannels() - } + self.updateChannels() self.checkForDeeplinks() } } @@ -340,7 +332,93 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController .filter { $0.id != chatClient.currentUserId } } - private func updateSearchResults() { + private func handleSearchTextChange() { + if searchText.isEmpty { + clearSearchResults() + return + } + + switch searchType { + case .messages: + performMessageSearch() + case .channels: + performChannelSearch() + default: + break + } + } + + private func loadAdditionalMessageSearchResults(index: Int) { + guard let messageSearchController = messageSearchController else { + return + } + + if index < messageSearchController.messages.count - 10 { + return + } + + if !loadingNextChannels { + loadingNextChannels = true + messageSearchController.loadNextMessages { [weak self] _ in + guard let self = self else { return } + self.loadingNextChannels = false + self.updateMessageSearchResults() + } + } + } + + private func loadAdditionalChannelSearchResults(index: Int) { + guard let channelListSearchController = self.channelListSearchController else { + return + } + + if index < channelListSearchController.channels.count - 10 { + return + } + + if !loadingNextChannels { + loadingNextChannels = true + channelListSearchController.loadNextChannels { [weak self] _ in + guard let self = self else { return } + self.loadingNextChannels = false + self.updateChannelSearchResults() + } + } + } + + private func performMessageSearch() { + guard let userId = chatClient.currentUserId else { return } + messageSearchController = chatClient.messageSearchController() + messageSearchController?.delegate = self + let query = MessageSearchQuery( + channelFilter: .containMembers(userIds: [userId]), + messageFilter: .autocomplete(.text, text: searchText) + ) + loadingSearchResults = true + messageSearchController?.search(query: query, completion: { [weak self] _ in + self?.loadingSearchResults = false + self?.updateMessageSearchResults() + }) + } + + private func performChannelSearch() { + guard let userId = chatClient.currentUserId else { return } + var query = ChannelListQuery( + filter: .and([ + .autocomplete(.name, text: searchText), + .containMembers(userIds: [userId]) + ]) + ) + query.options = [] + channelListSearchController = chatClient.channelListController(query: query) + loadingSearchResults = true + channelListSearchController?.synchronize { [weak self] _ in + self?.loadingSearchResults = false + self?.updateChannelSearchResults() + } + } + + private func updateMessageSearchResults() { guard let messageSearchController = messageSearchController else { return } @@ -351,26 +429,28 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController } } - private func handleSearchTextChange() { - if !searchText.isEmpty { - guard let userId = chatClient.currentUserId else { return } - messageSearchController = chatClient.messageSearchController() - messageSearchController?.delegate = self - let query = MessageSearchQuery( - channelFilter: .containMembers(userIds: [userId]), - messageFilter: .autocomplete(.text, text: searchText) - ) - loadingSearchResults = true - messageSearchController?.search(query: query, completion: { [weak self] _ in - self?.loadingSearchResults = false - self?.updateSearchResults() - }) - } else { - messageSearchController?.delegate = nil - messageSearchController = nil - searchResults = [] - updateChannels() + private func updateChannelSearchResults() { + guard let channelListSearchController = self.channelListSearchController else { + return } + + searchResults = channelListSearchController.channels + .compactMap { channel in + ChannelSelectionInfo( + channel: channel, + message: channel.previewMessage, + searchType: .channels + ) + } + } + + private func clearSearchResults() { + messageSearchController?.delegate = nil + messageSearchController = nil + channelListSearchController?.delegate = nil + channelListSearchController = nil + searchResults = [] + updateChannels() } private func observeClientIdChange() { @@ -491,3 +571,15 @@ public enum ChannelPopupType { /// Shows the 'more actions' popup. case moreActions(ChatChannel) } + +/// The type of data the channel list should perform a search. +public struct ChannelListSearchType: Equatable { + let type: String + + private init(type: String) { + self.type = type + } + + public static var channels = Self(type: "channels") + public static var messages = Self(type: "messages") +} diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelNavigatableListItem.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelNavigatableListItem.swift index f6e1ef97..439d6e35 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelNavigatableListItem.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelNavigatableListItem.swift @@ -73,10 +73,16 @@ public struct ChannelSelectionInfo: Identifiable { public let channel: ChatChannel public let message: ChatMessage? public var injectedChannelInfo: InjectedChannelInfo? + public var searchType: ChannelListSearchType - public init(channel: ChatChannel, message: ChatMessage?) { + public init( + channel: ChatChannel, + message: ChatMessage?, + searchType: ChannelListSearchType = .messages + ) { self.channel = channel self.message = message + self.searchType = searchType if let message = message { id = "\(channel.cid.id)-\(message.id)" } else { diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/DefaultChannelActions.swift b/Sources/StreamChatSwiftUI/ChatChannelList/DefaultChannelActions.swift index f1523e18..51c587ce 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/DefaultChannelActions.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/DefaultChannelActions.swift @@ -25,7 +25,7 @@ extension ChannelAction { actions.append(viewInfo) - if !channel.isDirectMessageChannel, let userId = chatClient.currentUserId { + if !channel.isDirectMessageChannel, channel.ownCapabilities.contains(.leaveChannel), let userId = chatClient.currentUserId { let leaveGroup = leaveGroup( for: channel, chatClient: chatClient, diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift index a5099cfb..5af48672 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift @@ -143,7 +143,7 @@ struct SearchResultItem: View { ChatTitleView(name: channelName) HStack { - SubtitleText(text: searchResult.message?.text ?? "") + SubtitleText(text: messageText) Spacer() SubtitleText(text: timestampText) } @@ -161,4 +161,18 @@ struct SearchResultItem: View { return "" } } + + private var messageText: String { + switch searchResult.searchType { + case .channels: + guard let previewMessage = searchResult.message else { + return L10n.Channel.Item.emptyMessages + } + return utils.messagePreviewFormatter.format(previewMessage) + case .messages: + return searchResult.message?.text ?? "" + default: + return "" + } + } } diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index c0920ed1..514b3b3a 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.65.0" + public static let version: String = "4.66.0" } diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist index 835d4500..bc6a72f5 100644 --- a/Sources/StreamChatSwiftUI/Info.plist +++ b/Sources/StreamChatSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.65.0 + 4.66.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPhotoLibraryUsageDescription diff --git a/Sources/StreamChatSwiftUI/Utils/Common/FileCDN.swift b/Sources/StreamChatSwiftUI/Utils/Common/FileCDN.swift index 477fc8e2..d502103b 100644 --- a/Sources/StreamChatSwiftUI/Utils/Common/FileCDN.swift +++ b/Sources/StreamChatSwiftUI/Utils/Common/FileCDN.swift @@ -5,7 +5,7 @@ import Foundation import StreamChat -/// A protocol the video preview uploader implementation must conform to. +/// FileCDN provides a set of functions to improve handling of files & videos from CDN. public protocol FileCDN: AnyObject { /// Prepare and return an adjusted or signed `URL` for the given file `URL` /// This function can be used to intercept an unsigned URL and return a valid signed URL diff --git a/Sources/StreamChatSwiftUI/Utils/Common/VideoPreviewLoader.swift b/Sources/StreamChatSwiftUI/Utils/Common/VideoPreviewLoader.swift index 1a27147d..366496c5 100644 --- a/Sources/StreamChatSwiftUI/Utils/Common/VideoPreviewLoader.swift +++ b/Sources/StreamChatSwiftUI/Utils/Common/VideoPreviewLoader.swift @@ -17,6 +17,8 @@ public protocol VideoPreviewLoader: AnyObject { /// The `VideoPreviewLoader` implemenation used by default. public final class DefaultVideoPreviewLoader: VideoPreviewLoader { + @Injected(\.utils) var utils + private let cache: Cache public init(countLimit: Int = 50) { @@ -39,26 +41,38 @@ public final class DefaultVideoPreviewLoader: VideoPreviewLoader { return call(completion, with: .success(cached)) } - let asset = AVURLAsset(url: url) - let imageGenerator = AVAssetImageGenerator(asset: asset) - let frameTime = CMTime(seconds: 0.1, preferredTimescale: 600) - - imageGenerator.appliesPreferredTrackTransform = true - imageGenerator.generateCGImagesAsynchronously(forTimes: [.init(time: frameTime)]) { [weak self] _, image, _, _, error in - guard let self = self else { return } - - let result: Result - if let thumbnail = image { - result = .success(.init(cgImage: thumbnail)) - } else if let error = error { - result = .failure(error) - } else { - log.error("Both error and image are `nil`.") + utils.fileCDN.adjustedURL(for: url) { result in + + let adjustedUrl: URL + switch result { + case let .success(url): + adjustedUrl = url + case let .failure(error): + self.call(completion, with: .failure(error)) return } - self.cache[url] = try? result.get() - self.call(completion, with: result) + let asset = AVURLAsset(url: adjustedUrl) + let imageGenerator = AVAssetImageGenerator(asset: asset) + let frameTime = CMTime(seconds: 0.1, preferredTimescale: 600) + + imageGenerator.appliesPreferredTrackTransform = true + imageGenerator.generateCGImagesAsynchronously(forTimes: [.init(time: frameTime)]) { [weak self] _, image, _, _, error in + guard let self = self else { return } + + let result: Result + if let thumbnail = image { + result = .success(.init(cgImage: thumbnail)) + } else if let error = error { + result = .failure(error) + } else { + log.error("Both error and image are `nil`.") + return + } + + self.cache[url] = try? result.get() + self.call(completion, with: result) + } } } diff --git a/Sources/StreamChatSwiftUI/Utils/SnapshotCreator.swift b/Sources/StreamChatSwiftUI/Utils/SnapshotCreator.swift index af4aed69..adcfedce 100644 --- a/Sources/StreamChatSwiftUI/Utils/SnapshotCreator.swift +++ b/Sources/StreamChatSwiftUI/Utils/SnapshotCreator.swift @@ -28,15 +28,9 @@ public class DefaultSnapshotCreator: SnapshotCreator { } func makeSnapshot(from view: UIView) -> UIImage { - let currentSnapshot: UIImage? - UIGraphicsBeginImageContext(view.frame.size) - if let currentGraphicsContext = UIGraphicsGetCurrentContext() { - view.layer.render(in: currentGraphicsContext) - currentSnapshot = UIGraphicsGetImageFromCurrentImageContext() - } else { - currentSnapshot = images.snapshot + let renderer = UIGraphicsImageRenderer(size: view.bounds.size) + return renderer.image { _ in + view.drawHierarchy(in: view.bounds, afterScreenUpdates: true) } - UIGraphicsEndImageContext() - return currentSnapshot ?? images.snapshot } } diff --git a/Sources/StreamChatSwiftUI/ViewModelsFactory.swift b/Sources/StreamChatSwiftUI/ViewModelsFactory.swift index 37280fb6..9b1ce724 100644 --- a/Sources/StreamChatSwiftUI/ViewModelsFactory.swift +++ b/Sources/StreamChatSwiftUI/ViewModelsFactory.swift @@ -14,14 +14,17 @@ public class ViewModelsFactory { /// - Parameters: /// - channelListController: possibility to inject custom channel list controller. /// - selectedChannelId: pre-selected channel id (used for deeplinking). + /// - searchType: The type of data the channel list should perform a search. By default it searches messages. /// - Returns: `ChatChannelListViewModel`. public static func makeChannelListViewModel( channelListController: ChatChannelListController? = nil, - selectedChannelId: String? = nil + selectedChannelId: String? = nil, + searchType: ChannelListSearchType = .messages ) -> ChatChannelListViewModel { ChatChannelListViewModel( channelListController: channelListController, - selectedChannelId: selectedChannelId + selectedChannelId: selectedChannelId, + searchType: searchType ) } diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 987a5bf2..14eb0b70 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.65.0' + spec.version = '4.66.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.65.0' + spec.dependency 'StreamChat-XCFramework', '~> 4.66.0' spec.cocoapods_version = '>= 1.11.0' end diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index d070eb36..e481276f 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI' - spec.version = '4.65.0' + spec.version = '4.66.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.65.0' + spec.dependency 'StreamChat', '~> 4.66.0' end diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index 353c0916..09620d43 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3185,7 +3185,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 4.8.0; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.iOS.StreamChatSwiftUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3230,7 +3230,7 @@ CODE_SIGN_ENTITLEMENTS = DemoAppSwiftUI/DemoAppSwiftUI.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DemoAppSwiftUI/Preview Content\""; DEVELOPMENT_TEAM = EHV7XZLAHA; ENABLE_PREVIEWS = YES; @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.57.0; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.iOS.DemoAppSwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3562,7 +3562,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 4.8.0; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.iOS.StreamChatSwiftUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3596,7 +3596,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 4.8.0; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.iOS.StreamChatSwiftUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3666,7 +3666,7 @@ CODE_SIGN_ENTITLEMENTS = DemoAppSwiftUI/DemoAppSwiftUI.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DemoAppSwiftUI/Preview Content\""; DEVELOPMENT_TEAM = EHV7XZLAHA; ENABLE_PREVIEWS = YES; @@ -3681,7 +3681,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.57.0; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.iOS.DemoAppSwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3700,7 +3700,7 @@ CODE_SIGN_ENTITLEMENTS = DemoAppSwiftUI/DemoAppSwiftUI.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DemoAppSwiftUI/Preview Content\""; DEVELOPMENT_TEAM = EHV7XZLAHA; ENABLE_PREVIEWS = YES; @@ -3716,7 +3716,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.57.0; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.iOS.DemoAppSwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.getstream.iOS.DemoAppSwiftUI"; @@ -3821,7 +3821,7 @@ repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.65.0; + minimumVersion = 4.66.0; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json index 68417199..f8359c53 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"} \ 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"} \ No newline at end of file diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift index 0c59a4e4..f11ea200 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift @@ -222,11 +222,23 @@ class ChatChannelInfoViewModel_Tests: StreamChatTestCase { XCTAssert(leaveButton == true) } + func test_chatChannelInfoVM_leaveButtonHiddenInGroup() { + // Given + let channel = mockGroup(with: 5, updateCapabilities: false) + let viewModel = ChatChannelInfoViewModel(channel: channel) + + // When + let leaveButton = viewModel.shouldShowLeaveConversationButton + + // Then + XCTAssert(leaveButton == false) + } + func test_chatChannelInfoVM_leaveButtonShownInDM() { // Given + let cidDM = ChannelId(type: .messaging, id: "!members" + .newUniqueId) let channel = ChatChannel.mock( - cid: .unique, - name: "Test", + cid: cidDM, ownCapabilities: [.deleteChannel] ) let viewModel = ChatChannelInfoViewModel(channel: channel) @@ -262,6 +274,7 @@ class ChatChannelInfoViewModel_Tests: StreamChatTestCase { if updateCapabilities { capabilities.insert(.updateChannel) capabilities.insert(.deleteChannel) + capabilities.insert(.leaveChannel) } let channel = ChatChannel.mock( cid: cid, diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoView_Tests.swift index ea3661a9..cd581ac9 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoView_Tests.swift @@ -103,7 +103,7 @@ class ChatChannelInfoView_Tests: StreamChatTestCase { let group = ChatChannel.mock( cid: .unique, name: "Test Group", - ownCapabilities: [.deleteChannel, .updateChannel], + ownCapabilities: [.leaveChannel, .updateChannel], lastActiveMembers: members, memberCount: members.count ) @@ -151,7 +151,7 @@ class ChatChannelInfoView_Tests: StreamChatTestCase { let group = ChatChannel.mock( cid: .unique, name: "Test Group", - ownCapabilities: [.deleteChannel, .updateChannel], + ownCapabilities: [.updateChannel, .leaveChannel], lastActiveMembers: members, memberCount: members.count ) diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift index 194d4fe8..b01623dc 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListViewModel_Tests.swift @@ -325,6 +325,42 @@ class ChatChannelListViewModel_Tests: StreamChatTestCase { cancellable.cancel() } + // MARK: - Search + + func test_loadAdditionalSearchResults_whenSearchTypeIsChannels_shouldLoadNextChannels() { + let searchChannelListController = makeChannelListController() + let viewModel = makeDefaultChannelListVM(searchType: .channels) + viewModel.channelListSearchController = searchChannelListController + + viewModel.loadAdditionalSearchResults(index: 1) + + XCTAssertEqual(searchChannelListController.loadNextChannelsCallCount, 1) + } + + func test_loadAdditionalSearchResults_whenSearchTypeIsMessages_shouldLoadNextMessages() { + let messageSearchController = ChatMessageSearchController_Mock.mock() + let viewModel = makeDefaultChannelListVM(searchType: .messages) + viewModel.messageSearchController = messageSearchController + + viewModel.loadAdditionalSearchResults(index: 1) + + XCTAssertEqual(messageSearchController.loadNextMessagesCallCount, 1) + } + + func test_searchText_whenChanged_whenSearchTypeIsChannels_shouldPerformChannelSearch() { + let viewModel = makeDefaultChannelListVM(searchType: .channels) + viewModel.searchText = "Hey" + XCTAssertNotNil(viewModel.channelListSearchController) + XCTAssertNil(viewModel.messageSearchController) + } + + func test_searchText_whenChanged_whenSearchTypeIsMessages_shouldPerformMessageSearch() { + let viewModel = makeDefaultChannelListVM(searchType: .messages) + viewModel.searchText = "Hey" + XCTAssertNil(viewModel.channelListSearchController) + XCTAssertNotNil(viewModel.messageSearchController) + } + // MARK: - private private func makeChannelListController( @@ -343,14 +379,15 @@ class ChatChannelListViewModel_Tests: StreamChatTestCase { } private func makeDefaultChannelListVM( - channels: [ChatChannel] = [] + channels: [ChatChannel] = [], + searchType: ChannelListSearchType = .messages ) -> ChatChannelListViewModel { let channelListController = makeChannelListController(channels: channels) let viewModel = ChatChannelListViewModel( channelListController: channelListController, - selectedChannelId: nil + selectedChannelId: nil, + searchType: searchType ) - return viewModel } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/SearchResultsView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/SearchResultsView_Tests.swift index cb5d0dff..e379b270 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannelList/SearchResultsView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannelList/SearchResultsView_Tests.swift @@ -53,6 +53,52 @@ class SearchResultsView_Tests: StreamChatTestCase { assertSnapshot(matching: view, as: .image) } + func test_searchResultsView_snapshotResults_whenChannelSearch() { + // Given + let channel1 = ChatChannel.mock(cid: .unique, name: "Test 1") + let message1 = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test 1", + author: .mock(id: .unique, name: "Luke") + ) + let result1 = ChannelSelectionInfo( + channel: channel1, + message: message1, + searchType: .channels + ) + let channel2 = ChatChannel.mock(cid: .unique, name: "Test 2") + let message2 = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test 2", + author: .mock(id: .unique, name: "Han Solo") + ) + let result2 = ChannelSelectionInfo( + channel: channel2, + message: message2, + searchType: .channels + ) + let searchResults = [result1, result2] + + // When + let view = SearchResultsView( + factory: DefaultViewFactory.shared, + selectedChannel: .constant(nil), + searchResults: searchResults, + loadingSearchResults: false, + onlineIndicatorShown: { _ in false }, + channelNaming: { $0.name ?? "" }, + imageLoader: { _ in UIImage(systemName: "person.circle")! }, + onSearchResultTap: { _ in }, + onItemAppear: { _ in } + ) + .applyDefaultSize() + + // Then + assertSnapshot(matching: view, as: .image) + } + func test_searchResultsView_snapshotNoResults() { // Given let searchResults = [ChannelSelectionInfo]() diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/SearchResultsView_Tests/test_searchResultsView_snapshotResults_whenChannelSearch.1.png b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/SearchResultsView_Tests/test_searchResultsView_snapshotResults_whenChannelSearch.1.png new file mode 100644 index 00000000..9287d48c Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/SearchResultsView_Tests/test_searchResultsView_snapshotResults_whenChannelSearch.1.png differ diff --git a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift index 3b8dffe5..5c44a692 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift @@ -643,7 +643,7 @@ extension MessageList_Tests { .scrollMessageListDown() // to hide the keyboard } THEN("user observes a preview of the video with description") { - userRobot.assertLinkPreview(alsoVerifyServiceName: "YouTube") + userRobot.assertLinkPreview() } } @@ -677,7 +677,7 @@ extension MessageList_Tests { userRobot.scrollMessageListDown() // to hide the keyboard } THEN("user observes a preview of the video with description") { - userRobot.assertLinkPreview(alsoVerifyServiceName: "YouTube") + userRobot.assertLinkPreview() } } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 240212be..187cc426 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -162,6 +162,19 @@ lane :match_me do |options| ) end +desc 'Builds the latest version of Demo app and uploads it to TestFlight' +lane :swiftui_testflight_build do + match_me + testflight_build( + api_key: appstore_api_key, + xcode_project: xcode_project, + sdk_target: 'StreamChatSwiftUI', + app_target: 'DemoAppSwiftUI', + app_identifier: 'io.getstream.iOS.DemoAppSwiftUI', + app_version: last_git_tag + ) +end + desc 'Runs tests in Debug config' lane :test_ui do |options| next unless is_check_required(sources: sources_matrix[:ui], force_check: @force_check) diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 3d5398c7..98410b08 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -5,4 +5,4 @@ gem 'fastlane-plugin-versioning' gem 'fastlane-plugin-sonarcloud_metric_kit' gem 'fastlane-plugin-create_xcframework' -gem 'fastlane-plugin-stream_actions', '0.3.70' +gem 'fastlane-plugin-stream_actions', '0.3.71' diff --git a/fastlane/testflight_export_options.plist b/fastlane/testflight_export_options.plist new file mode 100644 index 00000000..faa64f5c --- /dev/null +++ b/fastlane/testflight_export_options.plist @@ -0,0 +1,11 @@ + + + + + provisioningProfiles + + io.getstream.iOS.DemoAppSwiftUI + match AppStore io.getstream.iOS.DemoAppSwiftUI + + +