Skip to content

Commit 74da45e

Browse files
committed
Add ChatThreadListHeaderView to display new available threads
1 parent aa57d96 commit 74da45e

File tree

7 files changed

+154
-34
lines changed

7 files changed

+154
-34
lines changed

Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListErrorBannerView.swift

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,10 @@ public struct ChatThreadListErrorBannerView: View {
1111
let action: () -> Void
1212

1313
public var body: some View {
14-
HStack(alignment: .center) {
15-
Text(L10n.Thread.Error.message)
16-
.foregroundColor(Color(colors.staticColorText))
17-
Spacer()
18-
Button(action: action) {
19-
Image(uiImage: images.restart)
20-
.customizable()
21-
.frame(width: 20, height: 20)
22-
.foregroundColor(Color(colors.staticColorText))
23-
}
24-
}
25-
.padding(.all, 16)
26-
.background(Color(colors.bannerBackgroundColor))
14+
ActionBannerView(
15+
text: L10n.Thread.Error.message,
16+
image: images.restart,
17+
action: action
18+
)
2719
}
2820
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import SwiftUI
6+
7+
public struct ChatThreadListHeaderView: View {
8+
@Injected(\.colors) private var colors
9+
@Injected(\.images) private var images
10+
11+
@ObservedObject private var viewModel: ChatThreadListViewModel
12+
13+
init(
14+
viewModel: ChatThreadListViewModel
15+
) {
16+
self.viewModel = viewModel
17+
}
18+
19+
public var body: some View {
20+
Group {
21+
if viewModel.isReloading {
22+
LoadingView()
23+
.frame(height: 40)
24+
} else if viewModel.hasNewThreads {
25+
ActionBannerView(
26+
text: L10n.Thread.newThreads(viewModel.newThreadsCount),
27+
image: images.restart
28+
) {
29+
viewModel.loadThreads()
30+
}
31+
} else {
32+
EmptyView()
33+
}
34+
}
35+
}
36+
}

Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListView.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ public struct ChatThreadListView<Factory: ViewFactory>: View {
8383
)
8484
.modifier(viewFactory.makeThreadListHeaderViewModifier(title: title))
8585
.onAppear {
86-
if !viewModel.hasLoadedThreads {
87-
viewModel.loadThreads()
88-
}
86+
viewModel.viewDidAppear()
8987
}
9088
}
9189
}

Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListViewModel.swift

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ import Foundation
77
import StreamChat
88

99
/// View model for the `ChatThreadListView`.
10-
open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDelegate {
10+
open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDelegate, EventsControllerDelegate {
1111

1212
/// Context provided dependencies.
1313
@Injected(\.chatClient) private var chatClient: ChatClient
1414
@Injected(\.images) private var images: Images
1515
@Injected(\.utils) private var utils: Utils
1616

1717
/// The controller that manages the thread list data.
18-
private var controller: ChatThreadListController?
18+
private var threadListController: ChatThreadListController!
19+
20+
/// The controller that manages thread list events.
21+
private var eventsController: EventsController!
1922

2023
/// A boolean value indicating if the initial threads have been loaded.
2124
public private(set) var hasLoadedThreads = false
@@ -26,6 +29,9 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
2629
/// A boolean indicating if it is loading data from the server and no local cache is available.
2730
@Published public var isLoading = false
2831

32+
/// A boolean indicating if it is reloading data from the server.
33+
@Published public var isReloading = false
34+
2935
/// A boolean indicating that there is no data from server.
3036
@Published public var isEmpty = false
3137

@@ -41,18 +47,40 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
4147
/// A boolean value indicating if all the older threads are loaded.
4248
@Published public var hasLoadedAllThreads: Bool = false
4349

50+
/// The number of new threads available to be fetched.
51+
@Published public var newThreadsCount: Int = 0
52+
53+
/// A boolean value indicating if there are new threads available to be fetched.
54+
@Published public var hasNewThreads: Bool = false
55+
56+
/// The ids of the new threads available to be fetched.
57+
private var newAvailableThreadIds: Set<MessageId> = [] {
58+
didSet {
59+
newThreadsCount = newAvailableThreadIds.count
60+
hasNewThreads = newThreadsCount > 0
61+
}
62+
}
63+
4464
/// Creates a view model for the `ChatThreadListView`.
4565
///
4666
/// - Parameters:
4767
/// - threadListController: A controller providing the list of threads. If nil, a controller with default `ThreadListQuery` is created.
68+
/// - eventsController: The controller that manages thread list events. If nil, the default events controller will be provided.
4869
public init(
49-
threadListController: ChatThreadListController? = nil
70+
threadListController: ChatThreadListController? = nil,
71+
eventsController: EventsController? = nil
5072
) {
5173
if let threadListController = threadListController {
52-
self.controller = threadListController
74+
self.threadListController = threadListController
5375
} else {
5476
makeDefaultThreadListController()
5577
}
78+
79+
if let eventsController = eventsController {
80+
self.eventsController = eventsController
81+
} else {
82+
makeDefaultEventsController()
83+
}
5684
}
5785

5886
public func retryLoadThreads() {
@@ -64,16 +92,32 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
6492
loadMoreThreads()
6593
}
6694

95+
public func viewDidAppear() {
96+
if !hasLoadedThreads {
97+
startObserving()
98+
loadThreads()
99+
}
100+
}
101+
102+
public func startObserving() {
103+
threadListController.delegate = self
104+
eventsController?.delegate = self
105+
}
106+
67107
public func loadThreads() {
68-
controller?.delegate = self
69-
isLoading = controller?.threads.isEmpty == true
108+
isLoading = threadListController.threads.isEmpty == true
70109
failedToLoadThreads = false
71-
controller?.synchronize { [weak self] error in
110+
isReloading = !isEmpty
111+
threadListController.synchronize { [weak self] error in
72112
self?.isLoading = false
113+
self?.isReloading = false
73114
self?.hasLoadedThreads = error == nil
74115
self?.failedToLoadThreads = error != nil
75-
self?.isEmpty = self?.controller?.threads.isEmpty == true
76-
self?.hasLoadedAllThreads = self?.controller?.hasLoadedAllThreads ?? false
116+
self?.isEmpty = self?.threadListController.threads.isEmpty == true
117+
self?.hasLoadedAllThreads = self?.threadListController.hasLoadedAllThreads ?? false
118+
if error == nil {
119+
self?.newAvailableThreadIds = []
120+
}
77121
}
78122
}
79123

@@ -86,14 +130,14 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
86130
}
87131

88132
public func loadMoreThreads() {
89-
if isLoadingMoreThreads || controller?.hasLoadedAllThreads == true {
133+
if isLoadingMoreThreads || threadListController.hasLoadedAllThreads == true {
90134
return
91135
}
92136

93137
isLoadingMoreThreads = true
94-
controller?.loadMoreThreads { [weak self] result in
138+
threadListController.loadMoreThreads { [weak self] result in
95139
self?.isLoadingMoreThreads = false
96-
self?.hasLoadedAllThreads = self?.controller?.hasLoadedAllThreads ?? false
140+
self?.hasLoadedAllThreads = self?.threadListController.hasLoadedAllThreads ?? false
97141
let threads = try? result.get()
98142
self?.failedToLoadMoreThreads = threads == nil
99143
}
@@ -106,13 +150,26 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
106150
threads = controller.threads
107151
}
108152

109-
private func makeDefaultThreadListController() {
110-
guard let currentUserId = chatClient.currentUserId else {
111-
// TODO: observeClientIdChange()
112-
return
153+
public func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
154+
switch event {
155+
case let event as ThreadMessageNewEvent:
156+
guard let parentId = event.message.parentMessageId else { break }
157+
let isNewThread = threadListController.dataStore.thread(parentMessageId: parentId) == nil
158+
if isNewThread {
159+
newAvailableThreadIds.insert(parentId)
160+
}
161+
default:
162+
break
113163
}
114-
controller = chatClient.threadListController(
164+
}
165+
166+
private func makeDefaultThreadListController() {
167+
threadListController = chatClient.threadListController(
115168
query: .init(watch: true)
116169
)
117170
}
171+
172+
private func makeDefaultEventsController() {
173+
eventsController = chatClient.eventsController()
174+
}
118175
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import SwiftUI
6+
7+
struct ActionBannerView: View {
8+
@Injected(\.colors) private var colors
9+
10+
let text: String
11+
let image: UIImage
12+
let action: () -> Void
13+
14+
public var body: some View {
15+
HStack(alignment: .center) {
16+
Text(text)
17+
.foregroundColor(Color(colors.staticColorText))
18+
Spacer()
19+
Button(action: action) {
20+
Image(uiImage: image)
21+
.customizable()
22+
.frame(width: 20, height: 20)
23+
.foregroundColor(Color(colors.staticColorText))
24+
}
25+
}
26+
.padding(.all, 16)
27+
.background(Color(colors.bannerBackgroundColor))
28+
}
29+
}

Sources/StreamChatSwiftUI/DefaultViewFactory.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,7 @@ extension ViewFactory {
10081008
}
10091009

10101010
public func makeThreadListHeaderView(viewModel: ChatThreadListViewModel) -> some View {
1011-
EmptyView()
1011+
ChatThreadListHeaderView(viewModel: viewModel)
10121012
}
10131013

10141014
public func makeThreadListFooterView(viewModel: ChatThreadListViewModel) -> some View {

StreamChatSwiftUI.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,8 @@
518518
ADE0F55E2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */; };
519519
ADE0F5602CB846EC0053B8B9 /* FloatingBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */; };
520520
ADE0F5622CB8556F0053B8B9 /* ChatThreadListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */; };
521+
ADE0F5642CB9609E0053B8B9 /* ChatThreadListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5632CB9609E0053B8B9 /* ChatThreadListHeaderView.swift */; };
522+
ADE0F5662CB962470053B8B9 /* ActionBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5652CB962470053B8B9 /* ActionBannerView.swift */; };
521523
C14A465B284665B100EF498E /* SDKIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14A465A284665B100EF498E /* SDKIdentifier.swift */; };
522524
E3A1C01C282BAC66002D1E26 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = E3A1C01B282BAC66002D1E26 /* Sentry */; };
523525
/* End PBXBuildFile section */
@@ -1105,6 +1107,8 @@
11051107
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListErrorBannerView.swift; sourceTree = "<group>"; };
11061108
ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingBannerViewModifier.swift; sourceTree = "<group>"; };
11071109
ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListFooterView.swift; sourceTree = "<group>"; };
1110+
ADE0F5632CB9609E0053B8B9 /* ChatThreadListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderView.swift; sourceTree = "<group>"; };
1111+
ADE0F5652CB962470053B8B9 /* ActionBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBannerView.swift; sourceTree = "<group>"; };
11081112
C14A465A284665B100EF498E /* SDKIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKIdentifier.swift; sourceTree = "<group>"; };
11091113
/* End PBXFileReference section */
11101114

@@ -1693,6 +1697,7 @@
16931697
children = (
16941698
8465FCFA2746A95600AF091E /* ActionItemView.swift */,
16951699
4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */,
1700+
ADE0F5652CB962470053B8B9 /* ActionBannerView.swift */,
16961701
ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */,
16971702
4F077EF72C85E05700F06D83 /* DelayedRenderingViewModifier.swift */,
16981703
84AB7B1C2771F4AA00631A10 /* DiscardButtonView.swift */,
@@ -2247,6 +2252,7 @@
22472252
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */,
22482253
AD3AB6532CB54F310014D4D7 /* ChatThreadListItem.swift */,
22492254
AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */,
2255+
ADE0F5632CB9609E0053B8B9 /* ChatThreadListHeaderView.swift */,
22502256
ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */,
22512257
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */,
22522258
AD2DDA602CB040EA0040B8D4 /* NoThreadsView.swift */,
@@ -2595,6 +2601,7 @@
25952601
8465FDB22746A95700AF091E /* InputTextView.swift in Sources */,
25962602
8465FDB32746A95700AF091E /* NSLayoutConstraint+Extensions.swift in Sources */,
25972603
82D64BCF2AD7E5B700C5C79E /* LazyImageState.swift in Sources */,
2604+
ADE0F5642CB9609E0053B8B9 /* ChatThreadListHeaderView.swift in Sources */,
25982605
8465FD912746A95700AF091E /* MessageComposerView.swift in Sources */,
25992606
82D64BE52AD7E5B700C5C79E /* ImagePipelineCache.swift in Sources */,
26002607
8465FD6A2746A95700AF091E /* L10n.swift in Sources */,
@@ -2630,6 +2637,7 @@
26302637
AD3AB6582CB54F8C0014D4D7 /* ChatThreadListScreen.swift in Sources */,
26312638
82D64BDD2AD7E5B700C5C79E /* AnimatedImageView.swift in Sources */,
26322639
8434E58127707F19001E1B83 /* GridPhotosView.swift in Sources */,
2640+
ADE0F5662CB962470053B8B9 /* ActionBannerView.swift in Sources */,
26332641
84BB4C4C2841104700CBE004 /* MessageListDateUtils.swift in Sources */,
26342642
82D64BE72AD7E5B700C5C79E /* ImageTask.swift in Sources */,
26352643
8465FD742746A95700AF091E /* ViewFactory.swift in Sources */,

0 commit comments

Comments
 (0)