Stream Chat Unreal SDK
Loading...
Searching...
No Matches
SPaginateListWidget.h
1// Copyright 2026 Stream.IO, Inc. All Rights Reserved.
2
3#pragma once
4
5#include "Channel/ChatChannel.h"
6#include "CoreMinimal.h"
7#include "SStreamListView.h"
8
9template <class ItemType>
10class SPaginateListWidget : public SStreamListView<ItemType>
11{
12 using FSuperArguments = typename SStreamListView<ItemType>::FArguments;
13 using FCreateListViewWidgetDelegate = typename SStreamListView<ItemType>::FCreateListViewWidgetDelegate;
14
15 DECLARE_DELEGATE_TwoParams(FOnPaginatingDelegate, EPaginationDirection, EHttpRequestState);
16 DECLARE_DELEGATE_TwoParams(FDoPaginateDelegate, EPaginationDirection, TFunction<void()>);
17
18public:
19 SLATE_BEGIN_ARGS(SPaginateListWidget<ItemType>)
20 : _Limit{20}
21 , _PaginateScrollThreshold{100.f}
22 , _PaginationDirection{EPaginationDirection::Bottom}
23 , _DoPaginate{}
24 , _OnPaginating{}
25 , _ListItemsSource{nullptr}
26 , _CreateListViewWidget{}
27 {
28 }
29
30 SLATE_ARGUMENT(uint32, Limit)
31 SLATE_ARGUMENT(float, PaginateScrollThreshold)
32 SLATE_ARGUMENT(EPaginationDirection, PaginationDirection)
33 SLATE_EVENT(FDoPaginateDelegate, DoPaginate);
34 SLATE_EVENT(FOnPaginatingDelegate, OnPaginating);
35 SLATE_ARGUMENT(const TArray<ItemType>*, ListItemsSource)
36 SLATE_EVENT(FCreateListViewWidgetDelegate, CreateListViewWidget);
37
38 SLATE_END_ARGS()
39
40 void Construct(const FArguments& InArgs);
41 virtual STableViewBase::FReGenerateResults ReGenerateItems(const FGeometry& MyGeometry) override;
42
43protected:
44 void OnScroll(const double CurrentOffset);
45 void ConditionallyPaginate();
46 void Paginate(const EPaginationDirection Directions);
47
49 uint32 Limit = 20;
50 // If the scroll offset is below this value, then new messages will be fetched
51 float PaginateScrollThreshold = 100.f;
52 // The end of the scroll box which causes pagination to be triggered
53 EPaginationDirection PaginationDirection = EPaginationDirection::Bottom;
55 FDoPaginateDelegate DoPaginate;
57 FOnPaginatingDelegate OnPaginating;
58
59private:
60 void SetPaginationRequestState(const EHttpRequestState RequestState, const EPaginationDirection Direction);
61 EPaginationDirection GetDirections() const;
62 uint32 GetItemCount() const;
63
64 EPaginationDirection EndedPaginationDirections = EPaginationDirection::None;
65 EHttpRequestState PaginationRequestState = EHttpRequestState::Ended;
66 TOptional<ItemType> FirstItem;
67};
68
69template <class ItemType>
70void SPaginateListWidget<ItemType>::Construct(const FArguments& InArgs)
71{
72 SStreamListView<ItemType>::Construct(FSuperArguments()
73 .ListItemsSource(InArgs._ListItemsSource)
74 .CreateListViewWidget(InArgs._CreateListViewWidget)
75 .OnListViewScrolled(this, &SPaginateListWidget<ItemType>::OnScroll));
76
77 this->Limit = InArgs._Limit;
78 this->PaginateScrollThreshold = InArgs._PaginateScrollThreshold;
79 this->PaginationDirection = InArgs._PaginationDirection;
80 this->DoPaginate = InArgs._DoPaginate;
81 this->OnPaginating = InArgs._OnPaginating;
82}
83
84template <class ItemType>
85STableViewBase::FReGenerateResults SPaginateListWidget<ItemType>::ReGenerateItems(const FGeometry& MyGeometry)
86{
87 const STableViewBase::FReGenerateResults Result = SListView<ItemType>::ReGenerateItems(MyGeometry);
88
89 // Scroll to end on first population
90 if (!FirstItem.IsSet() && Result.LengthOfGeneratedItems > 0.)
91 {
92 if (PaginationDirection == EPaginationDirection::Top)
93 {
94 this->ScrollToBottom();
95 }
96 else if (PaginationDirection == EPaginationDirection::Bottom)
97 {
98 this->ScrollToTop();
99 OnScroll(this->CurrentScrollOffset);
100 }
101 }
102 return Result;
103}
104
105template <class ItemType>
106void SPaginateListWidget<ItemType>::OnScroll(const double CurrentOffset)
107{
108 const int32 FirstItemIndex = FMath::TruncToInt(CurrentOffset);
109 const TArrayView<const ItemType> Items = this->GetItems();
110 FirstItem = Items.IsValidIndex(FirstItemIndex) ? Items[FirstItemIndex] : TOptional<ItemType>{};
111
112 ConditionallyPaginate();
113}
114
115template <class ItemType>
116void SPaginateListWidget<ItemType>::ConditionallyPaginate()
117{
118 // Skip if a request is already in-flight
119 if (!DoPaginate.IsBound() || PaginationRequestState == EHttpRequestState::Started)
120 {
121 return;
122 }
123
124 // Skip if currently no data
125 if (this->GetItemCount() == 0)
126 {
127 return;
128 }
129
130 // Skip if not within the threshold of either end
131 const EPaginationDirection Directions = GetDirections();
132 if (Directions == EPaginationDirection::None)
133 {
134 return;
135 }
136
137 // Skip if the paginating in an unsupported direction
138 if (!EnumHasAnyFlags(Directions, PaginationDirection))
139 {
140 return;
141 }
142
143 // Skip if no more items will exist in this direction
144 if (EnumHasAllFlags(EndedPaginationDirections, Directions))
145 {
146 return;
147 }
148
149 Paginate(Directions);
150}
151
152template <class ItemType>
153void SPaginateListWidget<ItemType>::Paginate(const EPaginationDirection Directions)
154{
155 SetPaginationRequestState(EHttpRequestState::Started, Directions);
156
157 DoPaginate.Execute(
158 Directions,
159 [WeakThis = TWeakPtr<SWidget>(this->AsShared()), OrigWidgetCount = GetItemCount(), Directions]
160 {
161 if (!WeakThis.IsValid())
162 {
163 return;
164 }
165 TSharedPtr<SPaginateListWidget> This = StaticCastSharedPtr<SPaginateListWidget>(WeakThis.Pin());
166
167 const uint32 DeltaChildrenCount = This->GetItemCount() - OrigWidgetCount;
168 if (DeltaChildrenCount == 0 || DeltaChildrenCount < This->Limit)
169 {
170 // Don't need to paginate again in this direction in the future
171 This->EndedPaginationDirections |= Directions;
172 }
173
174 This->SetPaginationRequestState(EHttpRequestState::Ended, Directions);
175
176 if (This->FirstItem.IsSet())
177 {
178 const float FirstOffset = FMath::Fractional(This->DesiredScrollOffset);
179 const int32 NewIndex = This->GetItems().Find(This->FirstItem.GetValue());
180 This->ScrollTo(static_cast<float>(NewIndex) + FirstOffset);
181 }
182 });
183}
184
185template <class ItemType>
186void SPaginateListWidget<ItemType>::SetPaginationRequestState(const EHttpRequestState RequestState, const EPaginationDirection Direction)
187{
188 PaginationRequestState = RequestState;
189 OnPaginating.ExecuteIfBound(Direction, RequestState);
190}
191
192template <class ItemType>
193EPaginationDirection SPaginateListWidget<ItemType>::GetDirections() const
194{
195 EPaginationDirection Directions = EPaginationDirection::None;
196 const float Height = this->GetCachedGeometry().GetLocalSize().Y;
197 if (Height == 0.f)
198 {
199 return Directions;
200 }
201
202 const float TopOffset = this->ScrollBar->DistanceFromTop() * Height;
203 const float BottomOffset = this->ScrollBar->DistanceFromBottom() * Height;
204 if (TopOffset < PaginateScrollThreshold)
205 {
206 Directions |= EPaginationDirection::Top;
207 }
208 if (BottomOffset < PaginateScrollThreshold)
209 {
210 Directions |= EPaginationDirection::Bottom;
211 }
212 return Directions;
213}
214
215template <class ItemType>
216uint32 SPaginateListWidget<ItemType>::GetItemCount() const
217{
218 return this->GetItems().Num();
219}
@ None
Do nothing.