Skip to content

Commit 79f2afe

Browse files
committed
Add ChatThreadListFooterView + Loading More Theads
1 parent 6f7b535 commit 79f2afe

File tree

7 files changed

+131
-20
lines changed

7 files changed

+131
-20
lines changed

Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadList.swift

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,44 @@ import SwiftUI
77

88
/// Stateless component for the channel list.
99
/// If used directly, you should provide the thread list.
10-
public struct ThreadList<Factory: ViewFactory>: View {
10+
public struct ThreadList<Factory: ViewFactory, HeaderView: View, FooterView: View>: View {
1111
var threads: LazyCachedMapCollection<ChatThread>
1212
private var factory: Factory
1313
private var threadDestination: (ChatThread) -> Factory.ThreadDestination
14+
private var onItemAppear: (Int) -> Void
15+
16+
@ViewBuilder
17+
private var headerView: () -> HeaderView
18+
19+
@ViewBuilder
20+
private var footerView: () -> FooterView
1421

1522
public init(
1623
factory: Factory,
1724
threads: LazyCachedMapCollection<ChatThread>,
18-
threadDestination: @escaping (ChatThread) -> Factory.ThreadDestination
25+
threadDestination: @escaping (ChatThread) -> Factory.ThreadDestination,
26+
onItemAppear: @escaping (Int) -> Void,
27+
headerView: @escaping () -> HeaderView,
28+
footerView: @escaping () -> FooterView
1929
) {
2030
self.factory = factory
2131
self.threads = threads
2232
self.threadDestination = threadDestination
33+
self.onItemAppear = onItemAppear
34+
self.headerView = headerView
35+
self.footerView = footerView
2336
}
2437

2538
public var body: some View {
2639
ScrollView {
40+
headerView()
2741
ThreadsLazyVStack(
2842
factory: factory,
2943
threads: threads,
30-
threadDestination: threadDestination
44+
threadDestination: threadDestination,
45+
onItemAppear: onItemAppear
3146
)
47+
footerView()
3248
}
3349
}
3450
}
@@ -38,15 +54,18 @@ public struct ThreadsLazyVStack<Factory: ViewFactory>: View {
3854
private var factory: Factory
3955
var threads: LazyCachedMapCollection<ChatThread>
4056
private var threadDestination: (ChatThread) -> Factory.ThreadDestination
57+
private var onItemAppear: (Int) -> Void
4158

4259
public init(
4360
factory: Factory,
4461
threads: LazyCachedMapCollection<ChatThread>,
45-
threadDestination: @escaping (ChatThread) -> Factory.ThreadDestination
62+
threadDestination: @escaping (ChatThread) -> Factory.ThreadDestination,
63+
onItemAppear: @escaping (Int) -> Void
4664
) {
4765
self.factory = factory
4866
self.threads = threads
4967
self.threadDestination = threadDestination
68+
self.onItemAppear = onItemAppear
5069
}
5170

5271
public var body: some View {
@@ -56,10 +75,16 @@ public struct ThreadsLazyVStack<Factory: ViewFactory>: View {
5675
thread: thread,
5776
threadDestination: threadDestination
5877
)
78+
.onAppear {
79+
if let index = threads.firstIndex(where: { chatThread in
80+
chatThread.id == thread.id
81+
}) {
82+
onItemAppear(index)
83+
}
84+
}
5985
factory.makeThreadListDividerItem()
6086
}
6187
}
62-
.modifier(factory.makeThreadListModifier())
6388
}
6489
}
6590

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import SwiftUI
6+
7+
public struct ChatThreadListFooterView: View {
8+
@ObservedObject private var viewModel: ChatThreadListViewModel
9+
10+
init(
11+
viewModel: ChatThreadListViewModel
12+
) {
13+
self.viewModel = viewModel
14+
}
15+
16+
public var body: some View {
17+
Group {
18+
if viewModel.isLoadingMoreThreads {
19+
LoadingView()
20+
.frame(height: 40)
21+
} else {
22+
EmptyView()
23+
}
24+
}
25+
}
26+
}

Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListView.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ public struct ChatThreadListView<Factory: ViewFactory>: View {
7272
)
7373
}
7474
}
75-
.bottomBanner(isPresented: viewModel.failedToLoadThreads) {
75+
.bottomBanner(isPresented: viewModel.failedToLoadThreads || viewModel.failedToLoadMoreThreads) {
7676
viewFactory.makeThreadsListErrorBannerView {
77-
viewModel.loadThreads()
77+
viewModel.retryLoadThreads()
7878
}
7979
}
8080
.accentColor(colors.tintColor)
@@ -113,7 +113,16 @@ public struct ChatThreadListContentView<Factory: ViewFactory>: View {
113113
ThreadList(
114114
factory: viewFactory,
115115
threads: viewModel.threads,
116-
threadDestination: viewFactory.makeThreadDestination()
116+
threadDestination: viewFactory.makeThreadDestination(),
117+
onItemAppear: { index in
118+
viewModel.didAppearThread(at: index)
119+
},
120+
headerView: {
121+
viewFactory.makeThreadListHeaderView(viewModel: viewModel)
122+
},
123+
footerView: {
124+
viewFactory.makeThreadListFooterView(viewModel: viewModel)
125+
}
117126
)
118127
}
119128
}

Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListViewModel.swift

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
1717
/// The controller that manages the thread list data.
1818
private var controller: ChatThreadListController?
1919

20-
/// A boolean value indicating if the view is currently loading more threads.
21-
public private(set) var loadingMoreThreads: Bool = false
22-
2320
/// A boolean value indicating if the initial threads have been loaded.
2421
public private(set) var hasLoadedThreads = false
2522

@@ -34,6 +31,16 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
3431

3532
/// A boolean indicating if it failed loading the initial data from the server.
3633
@Published public var failedToLoadThreads = false
34+
35+
/// A boolean indicating if it failed loading threads while paginating.
36+
@Published public var failedToLoadMoreThreads = false
37+
38+
/// A boolean value indicating if the view is currently loading more threads.
39+
@Published public var isLoadingMoreThreads: Bool = false
40+
41+
/// A boolean value indicating if all the older threads are loaded.
42+
@Published public var hasLoadedAllThreads: Bool = false
43+
3744
/// Creates a view model for the `ChatThreadListView`.
3845
///
3946
/// - Parameters:
@@ -48,6 +55,15 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
4855
}
4956
}
5057

58+
public func retryLoadThreads() {
59+
if failedToLoadThreads {
60+
loadThreads()
61+
return
62+
}
63+
64+
loadMoreThreads()
65+
}
66+
5167
public func loadThreads() {
5268
controller?.delegate = self
5369
isLoading = controller?.threads.isEmpty == true
@@ -57,6 +73,29 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe
5773
self?.hasLoadedThreads = error == nil
5874
self?.failedToLoadThreads = error != nil
5975
self?.isEmpty = self?.controller?.threads.isEmpty == true
76+
self?.hasLoadedAllThreads = self?.controller?.hasLoadedAllThreads ?? false
77+
}
78+
}
79+
80+
public func didAppearThread(at index: Int) {
81+
guard index >= threads.count - 5 else {
82+
return
83+
}
84+
85+
loadMoreThreads()
86+
}
87+
88+
public func loadMoreThreads() {
89+
if isLoadingMoreThreads || controller?.hasLoadedAllThreads == true {
90+
return
91+
}
92+
93+
isLoadingMoreThreads = true
94+
controller?.loadMoreThreads { [weak self] result in
95+
self?.isLoadingMoreThreads = false
96+
self?.hasLoadedAllThreads = self?.controller?.hasLoadedAllThreads ?? false
97+
let threads = try? result.get()
98+
self?.failedToLoadMoreThreads = threads == nil
6099
}
61100
}
62101

Sources/StreamChatSwiftUI/DefaultViewFactory.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,15 +1007,19 @@ extension ViewFactory {
10071007
ChatThreadListHeaderViewModifier(title: title)
10081008
}
10091009

1010+
public func makeThreadListHeaderView(viewModel: ChatThreadListViewModel) -> some View {
1011+
EmptyView()
1012+
}
1013+
1014+
public func makeThreadListFooterView(viewModel: ChatThreadListViewModel) -> some View {
1015+
ChatThreadListFooterView(viewModel: viewModel)
1016+
}
1017+
10101018
public func makeThreadListBackground(colors: ColorPalette) -> some View {
10111019
Color(colors.background)
10121020
.edgesIgnoringSafeArea(.bottom)
10131021
}
10141022

1015-
public func makeThreadListModifier() -> some ViewModifier {
1016-
EmptyViewModifier()
1017-
}
1018-
10191023
public func makeThreadListDividerItem() -> some View {
10201024
Divider()
10211025
}

Sources/StreamChatSwiftUI/ViewFactory.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,13 +1017,17 @@ public protocol ViewFactory: AnyObject {
10171017
func makeThreadListLoadingView() -> ThreadListLoadingView
10181018

10191019
associatedtype ThreadListHeaderViewModifier: ViewModifier
1020-
/// Creates the thread list header view modifier.
1020+
/// Creates the thread list navigation header view modifier.
10211021
/// - Parameter title: the title displayed in the header.
10221022
func makeThreadListHeaderViewModifier(title: String) -> ThreadListHeaderViewModifier
10231023

1024-
associatedtype ThreadListModifier: ViewModifier
1025-
/// Returns a view modifier applied to the thread list.
1026-
func makeThreadListModifier() -> ThreadListModifier
1024+
associatedtype ThreadListHeaderView: View
1025+
/// Creates the header view for the thread list.
1026+
func makeThreadListHeaderView(viewModel: ChatThreadListViewModel) -> ThreadListHeaderView
1027+
1028+
associatedtype ThreadListFooterView: View
1029+
/// Creates the footer view for the thread list.
1030+
func makeThreadListFooterView(viewModel: ChatThreadListViewModel) -> ThreadListFooterView
10271031

10281032
associatedtype ThreadListBackground: View
10291033
/// Creates the background for the thread list.

StreamChatSwiftUI.xcodeproj/project.pbxproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@
517517
AD3AB6602CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */; };
518518
ADE0F55E2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */; };
519519
ADE0F5602CB846EC0053B8B9 /* FloatingBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */; };
520+
ADE0F5622CB8556F0053B8B9 /* ChatThreadListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */; };
520521
C14A465B284665B100EF498E /* SDKIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14A465A284665B100EF498E /* SDKIdentifier.swift */; };
521522
E3A1C01C282BAC66002D1E26 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = E3A1C01B282BAC66002D1E26 /* Sentry */; };
522523
/* End PBXBuildFile section */
@@ -1103,6 +1104,7 @@
11031104
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderViewModifier.swift; sourceTree = "<group>"; };
11041105
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListErrorBannerView.swift; sourceTree = "<group>"; };
11051106
ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingBannerViewModifier.swift; sourceTree = "<group>"; };
1107+
ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListFooterView.swift; sourceTree = "<group>"; };
11061108
C14A465A284665B100EF498E /* SDKIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKIdentifier.swift; sourceTree = "<group>"; };
11071109
/* End PBXFileReference section */
11081110

@@ -2242,9 +2244,10 @@
22422244
AD2DDA5C2CB033460040B8D4 /* ChatThreadList.swift */,
22432245
AD2DDA5E2CB0361B0040B8D4 /* ChatThreadListViewModel.swift */,
22442246
AD3AB6552CB54F720014D4D7 /* ChatThreadListNavigatableItem.swift */,
2247+
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */,
22452248
AD3AB6532CB54F310014D4D7 /* ChatThreadListItem.swift */,
22462249
AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */,
2247-
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */,
2250+
ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */,
22482251
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */,
22492252
AD2DDA602CB040EA0040B8D4 /* NoThreadsView.swift */,
22502253
);
@@ -2711,6 +2714,7 @@
27112714
845CFD782BDA6BFD0058F691 /* PollResultsView.swift in Sources */,
27122715
8465FD892746A95700AF091E /* ComposerTextInputView.swift in Sources */,
27132716
8465FDBC2746A95700AF091E /* ChannelAvatarsMerger.swift in Sources */,
2717+
ADE0F5622CB8556F0053B8B9 /* ChatThreadListFooterView.swift in Sources */,
27142718
8465FDB82746A95700AF091E /* ImageMerger.swift in Sources */,
27152719
82D64BF22AD7E5B700C5C79E /* ImageProcessors+RoundedCorners.swift in Sources */,
27162720
841B64C427744DB60016FF3B /* ComposerModels.swift in Sources */,

0 commit comments

Comments
 (0)