Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### ✅ 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)

Expand Down
12 changes: 9 additions & 3 deletions DemoAppSwiftUI/DemoAppSwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ struct DemoAppSwiftUIApp: App {
var channelListController: ChatChannelListController? {
appState.channelListController
}


var channelListSearchType: ChannelListSearchType {
.messages
}

var body: some Scene {
WindowGroup {
switch appState.userState {
Expand Down Expand Up @@ -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
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct ChatChannelListView<Factory: ViewFactory>: View {
private let customOnItemTap: ((ChatChannel) -> Void)?
private var embedInNavigationView: Bool
private var handleTabBarVisibility: Bool

/// Creates a channel list view.
///
/// - Parameters:
Expand All @@ -31,6 +31,7 @@ public struct ChatChannelListView<Factory: ViewFactory>: 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
Expand All @@ -47,12 +48,14 @@ public struct ChatChannelListView<Factory: ViewFactory>: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
/// Temporarly holding changes while message list is shown.
private var queuedChannelsChanges = LazyCachedMapCollection<ChatChannel>()

private var messageSearchController: ChatMessageSearchController?

private var timer: Timer?

/// Controls loading the channels.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -258,7 +254,7 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
// MARK: - ChatMessageSearchControllerDelegate

public func controller(_ controller: ChatMessageSearchController, didChangeMessages changes: [ListChange<ChatMessage>]) {
updateSearchResults()
updateMessageSearchResults()
}

// MARK: - private
Expand Down Expand Up @@ -340,7 +336,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
}
Expand All @@ -351,26 +433,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() {
Expand Down Expand Up @@ -491,3 +575,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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ struct SearchResultItem<ChannelDestination: View>: View {
ChatTitleView(name: channelName)

HStack {
SubtitleText(text: searchResult.message?.text ?? "")
SubtitleText(text: messageText)
Spacer()
SubtitleText(text: timestampText)
}
Expand All @@ -161,4 +161,18 @@ struct SearchResultItem<ChannelDestination: View>: 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 ""
}
}
}
7 changes: 5 additions & 2 deletions Sources/StreamChatSwiftUI/ViewModelsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}

Expand Down
Loading
Loading