Skip to content

Commit 4f6ed70

Browse files
authored
Add support for Channel Search in the Channel List (#628)
* Add support for channel search in the Channel List component * Update CHANGELOG.md
1 parent 561be6f commit 4f6ed70

File tree

10 files changed

+269
-56
lines changed

10 files changed

+269
-56
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6+
### ✅ Added
7+
- Add support for Channel Search in the Channel List [#628](https://github.com/GetStream/stream-chat-swiftui/pull/628)
68
### 🐞 Fixed
79
- Fix crash when opening message overlay in iPad with a TabBar [#627](https://github.com/GetStream/stream-chat-swiftui/pull/627)
810

DemoAppSwiftUI/DemoAppSwiftUIApp.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ struct DemoAppSwiftUIApp: App {
1818
var channelListController: ChatChannelListController? {
1919
appState.channelListController
2020
}
21-
21+
22+
var channelListSearchType: ChannelListSearchType {
23+
.messages
24+
}
25+
2226
var body: some Scene {
2327
WindowGroup {
2428
switch appState.userState {
@@ -64,12 +68,14 @@ struct DemoAppSwiftUIApp: App {
6468
ChatChannelListView(
6569
viewFactory: DemoAppFactory.shared,
6670
channelListController: channelListController,
67-
selectedChannelId: notificationsHandler.notificationChannelId
71+
selectedChannelId: notificationsHandler.notificationChannelId,
72+
searchType: channelListSearchType
6873
)
6974
} else {
7075
ChatChannelListView(
7176
viewFactory: DemoAppFactory.shared,
72-
channelListController: channelListController
77+
channelListController: channelListController,
78+
searchType: channelListSearchType
7379
)
7480
}
7581
}

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public struct ChatChannelListView<Factory: ViewFactory>: View {
1919
private let customOnItemTap: ((ChatChannel) -> Void)?
2020
private var embedInNavigationView: Bool
2121
private var handleTabBarVisibility: Bool
22-
22+
2323
/// Creates a channel list view.
2424
///
2525
/// - Parameters:
@@ -31,6 +31,7 @@ public struct ChatChannelListView<Factory: ViewFactory>: View {
3131
/// - selectedChannelId: The id of a channel to be opened after the initial channel list load.
3232
/// - handleTabBarVisibility: True, if TabBar visibility should be automatically updated.
3333
/// - embedInNavigationView: True, if the channel list view should be embedded in a navigation stack.
34+
/// - searchType: The type of data the channel list should perform a search. By default it searches messages.
3435
///
3536
/// 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.
3637
/// ```swift
@@ -47,12 +48,14 @@ public struct ChatChannelListView<Factory: ViewFactory>: View {
4748
onItemTap: ((ChatChannel) -> Void)? = nil,
4849
selectedChannelId: String? = nil,
4950
handleTabBarVisibility: Bool = true,
50-
embedInNavigationView: Bool = true
51+
embedInNavigationView: Bool = true,
52+
searchType: ChannelListSearchType = .messages
5153
) {
5254
_viewModel = StateObject(
5355
wrappedValue: viewModel ?? ViewModelsFactory.makeChannelListViewModel(
5456
channelListController: channelListController,
55-
selectedChannelId: selectedChannelId
57+
selectedChannelId: selectedChannelId,
58+
searchType: searchType
5659
)
5760
)
5861
self.viewFactory = viewFactory

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListViewModel.swift

Lines changed: 139 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
3030
/// Temporarly holding changes while message list is shown.
3131
private var queuedChannelsChanges = LazyCachedMapCollection<ChatChannel>()
3232

33-
private var messageSearchController: ChatMessageSearchController?
34-
3533
private var timer: Timer?
3634

3735
/// Controls loading the channels.
@@ -103,6 +101,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
103101
}
104102
}
105103

104+
private let searchType: ChannelListSearchType
105+
internal var channelListSearchController: ChatChannelListController?
106+
internal var messageSearchController: ChatMessageSearchController?
107+
108+
@Published public var loadingSearchResults = false
109+
@Published public var searchResults = [ChannelSelectionInfo]()
110+
@Published var hideTabBar = false
106111
@Published public var searchText = "" {
107112
didSet {
108113
if searchText != oldValue {
@@ -111,10 +116,6 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
111116
}
112117
}
113118

114-
@Published public var loadingSearchResults = false
115-
@Published public var searchResults = [ChannelSelectionInfo]()
116-
@Published var hideTabBar = false
117-
118119
public var isSearching: Bool {
119120
!searchText.isEmpty
120121
}
@@ -125,10 +126,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
125126
/// - channelListController: A controller providing the list of channels. If nil, a controller with default `ChannelListQuery` is created.
126127
/// - selectedChannelId: The id of a channel to select. If the channel is not part of the channel list query, no channel is selected.
127128
/// Consider using ``ChatChannelScreen`` for presenting channels what might not be part of the initial page of channels.
129+
/// - searchType: The type of data the channel list should perform a search.
128130
public init(
129131
channelListController: ChatChannelListController? = nil,
130-
selectedChannelId: String? = nil
132+
selectedChannelId: String? = nil,
133+
searchType: ChannelListSearchType = .channels
131134
) {
135+
self.searchType = searchType
132136
self.selectedChannelId = selectedChannelId
133137
if let channelListController = channelListController {
134138
controller = channelListController
@@ -168,21 +172,13 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
168172
}
169173

170174
public func loadAdditionalSearchResults(index: Int) {
171-
guard let messageSearchController = messageSearchController else {
172-
return
173-
}
174-
175-
if index < messageSearchController.messages.count - 10 {
176-
return
177-
}
178-
179-
if !loadingNextChannels {
180-
loadingNextChannels = true
181-
messageSearchController.loadNextMessages { [weak self] _ in
182-
guard let self = self else { return }
183-
self.loadingNextChannels = false
184-
self.updateSearchResults()
185-
}
175+
switch searchType {
176+
case .channels:
177+
loadAdditionalChannelSearchResults(index: index)
178+
case .messages:
179+
loadAdditionalMessageSearchResults(index: index)
180+
default:
181+
break
186182
}
187183
}
188184

@@ -258,7 +254,7 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
258254
// MARK: - ChatMessageSearchControllerDelegate
259255

260256
public func controller(_ controller: ChatMessageSearchController, didChangeMessages changes: [ListChange<ChatMessage>]) {
261-
updateSearchResults()
257+
updateMessageSearchResults()
262258
}
263259

264260
// MARK: - private
@@ -340,7 +336,93 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
340336
.filter { $0.id != chatClient.currentUserId }
341337
}
342338

343-
private func updateSearchResults() {
339+
private func handleSearchTextChange() {
340+
if searchText.isEmpty {
341+
clearSearchResults()
342+
return
343+
}
344+
345+
switch searchType {
346+
case .messages:
347+
performMessageSearch()
348+
case .channels:
349+
performChannelSearch()
350+
default:
351+
break
352+
}
353+
}
354+
355+
private func loadAdditionalMessageSearchResults(index: Int) {
356+
guard let messageSearchController = messageSearchController else {
357+
return
358+
}
359+
360+
if index < messageSearchController.messages.count - 10 {
361+
return
362+
}
363+
364+
if !loadingNextChannels {
365+
loadingNextChannels = true
366+
messageSearchController.loadNextMessages { [weak self] _ in
367+
guard let self = self else { return }
368+
self.loadingNextChannels = false
369+
self.updateMessageSearchResults()
370+
}
371+
}
372+
}
373+
374+
private func loadAdditionalChannelSearchResults(index: Int) {
375+
guard let channelListSearchController = self.channelListSearchController else {
376+
return
377+
}
378+
379+
if index < channelListSearchController.channels.count - 10 {
380+
return
381+
}
382+
383+
if !loadingNextChannels {
384+
loadingNextChannels = true
385+
channelListSearchController.loadNextChannels { [weak self] _ in
386+
guard let self = self else { return }
387+
self.loadingNextChannels = false
388+
self.updateChannelSearchResults()
389+
}
390+
}
391+
}
392+
393+
private func performMessageSearch() {
394+
guard let userId = chatClient.currentUserId else { return }
395+
messageSearchController = chatClient.messageSearchController()
396+
messageSearchController?.delegate = self
397+
let query = MessageSearchQuery(
398+
channelFilter: .containMembers(userIds: [userId]),
399+
messageFilter: .autocomplete(.text, text: searchText)
400+
)
401+
loadingSearchResults = true
402+
messageSearchController?.search(query: query, completion: { [weak self] _ in
403+
self?.loadingSearchResults = false
404+
self?.updateMessageSearchResults()
405+
})
406+
}
407+
408+
private func performChannelSearch() {
409+
guard let userId = chatClient.currentUserId else { return }
410+
var query = ChannelListQuery(
411+
filter: .and([
412+
.autocomplete(.name, text: searchText),
413+
.containMembers(userIds: [userId])
414+
])
415+
)
416+
query.options = []
417+
channelListSearchController = chatClient.channelListController(query: query)
418+
loadingSearchResults = true
419+
channelListSearchController?.synchronize { [weak self] _ in
420+
self?.loadingSearchResults = false
421+
self?.updateChannelSearchResults()
422+
}
423+
}
424+
425+
private func updateMessageSearchResults() {
344426
guard let messageSearchController = messageSearchController else {
345427
return
346428
}
@@ -351,26 +433,28 @@ open class ChatChannelListViewModel: ObservableObject, ChatChannelListController
351433
}
352434
}
353435

354-
private func handleSearchTextChange() {
355-
if !searchText.isEmpty {
356-
guard let userId = chatClient.currentUserId else { return }
357-
messageSearchController = chatClient.messageSearchController()
358-
messageSearchController?.delegate = self
359-
let query = MessageSearchQuery(
360-
channelFilter: .containMembers(userIds: [userId]),
361-
messageFilter: .autocomplete(.text, text: searchText)
362-
)
363-
loadingSearchResults = true
364-
messageSearchController?.search(query: query, completion: { [weak self] _ in
365-
self?.loadingSearchResults = false
366-
self?.updateSearchResults()
367-
})
368-
} else {
369-
messageSearchController?.delegate = nil
370-
messageSearchController = nil
371-
searchResults = []
372-
updateChannels()
436+
private func updateChannelSearchResults() {
437+
guard let channelListSearchController = self.channelListSearchController else {
438+
return
373439
}
440+
441+
searchResults = channelListSearchController.channels
442+
.compactMap { channel in
443+
ChannelSelectionInfo(
444+
channel: channel,
445+
message: channel.previewMessage,
446+
searchType: .channels
447+
)
448+
}
449+
}
450+
451+
private func clearSearchResults() {
452+
messageSearchController?.delegate = nil
453+
messageSearchController = nil
454+
channelListSearchController?.delegate = nil
455+
channelListSearchController = nil
456+
searchResults = []
457+
updateChannels()
374458
}
375459

376460
private func observeClientIdChange() {
@@ -491,3 +575,15 @@ public enum ChannelPopupType {
491575
/// Shows the 'more actions' popup.
492576
case moreActions(ChatChannel)
493577
}
578+
579+
/// The type of data the channel list should perform a search.
580+
public struct ChannelListSearchType: Equatable {
581+
let type: String
582+
583+
private init(type: String) {
584+
self.type = type
585+
}
586+
587+
public static var channels = Self(type: "channels")
588+
public static var messages = Self(type: "messages")
589+
}

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelNavigatableListItem.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,16 @@ public struct ChannelSelectionInfo: Identifiable {
7373
public let channel: ChatChannel
7474
public let message: ChatMessage?
7575
public var injectedChannelInfo: InjectedChannelInfo?
76+
public var searchType: ChannelListSearchType
7677

77-
public init(channel: ChatChannel, message: ChatMessage?) {
78+
public init(
79+
channel: ChatChannel,
80+
message: ChatMessage?,
81+
searchType: ChannelListSearchType = .messages
82+
) {
7883
self.channel = channel
7984
self.message = message
85+
self.searchType = searchType
8086
if let message = message {
8187
id = "\(channel.cid.id)-\(message.id)"
8288
} else {

Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ struct SearchResultItem<ChannelDestination: View>: View {
143143
ChatTitleView(name: channelName)
144144

145145
HStack {
146-
SubtitleText(text: searchResult.message?.text ?? "")
146+
SubtitleText(text: messageText)
147147
Spacer()
148148
SubtitleText(text: timestampText)
149149
}
@@ -161,4 +161,18 @@ struct SearchResultItem<ChannelDestination: View>: View {
161161
return ""
162162
}
163163
}
164+
165+
private var messageText: String {
166+
switch searchResult.searchType {
167+
case .channels:
168+
guard let previewMessage = searchResult.message else {
169+
return L10n.Channel.Item.emptyMessages
170+
}
171+
return utils.messagePreviewFormatter.format(previewMessage)
172+
case .messages:
173+
return searchResult.message?.text ?? ""
174+
default:
175+
return ""
176+
}
177+
}
164178
}

Sources/StreamChatSwiftUI/ViewModelsFactory.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ public class ViewModelsFactory {
1414
/// - Parameters:
1515
/// - channelListController: possibility to inject custom channel list controller.
1616
/// - selectedChannelId: pre-selected channel id (used for deeplinking).
17+
/// - searchType: The type of data the channel list should perform a search. By default it searches messages.
1718
/// - Returns: `ChatChannelListViewModel`.
1819
public static func makeChannelListViewModel(
1920
channelListController: ChatChannelListController? = nil,
20-
selectedChannelId: String? = nil
21+
selectedChannelId: String? = nil,
22+
searchType: ChannelListSearchType = .messages
2123
) -> ChatChannelListViewModel {
2224
ChatChannelListViewModel(
2325
channelListController: channelListController,
24-
selectedChannelId: selectedChannelId
26+
selectedChannelId: selectedChannelId,
27+
searchType: searchType
2528
)
2629
}
2730

0 commit comments

Comments
 (0)