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