[frontend] barebones virtual scroller implementation

This commit is contained in:
Lilian 2024-06-08 16:31:41 +02:00
parent 92ee62caf2
commit 9dbbbfcc53
No known key found for this signature in database
GPG key ID: 007CA12D692829E1
5 changed files with 265 additions and 31 deletions

View file

@ -7,23 +7,14 @@
@inject ApiService ApiService @inject ApiService ApiService
<div @ref="Scroller" class="scroller">
@if (_init) @if (_init)
{ {
@foreach (var note in Timeline) <VirtualScroller NoteResponseList="Timeline" ReachedEnd="FetchOlder" ReachedStart="FetchOlder" />
{
<LazyNote Scroller="Scroller" Note="note"/>
}
<IntersectionObserve Options="new IntersectionObserverOptions() { Root = Scroller, RootMargin = _rootMagin }" OnChange="entry => OnEnd(entry)">
<div @ref="context.Ref.Current" class="end">END!</div>
</IntersectionObserve>
} }
else else
{ {
<div>Loading</div> <div>Loading</div>
} }
</div>
@code { @code {
private List<NoteResponse> Timeline { get; set; } = []; private List<NoteResponse> Timeline { get; set; } = [];
@ -31,12 +22,10 @@
private string? MaxId { get; set; } private string? MaxId { get; set; }
private string? MinId { get; set; } private string? MinId { get; set; }
private bool LockFetch { get; set; } private bool LockFetch { get; set; }
public ElementReference Scroller { get; set; }
private string _rootMagin = "100%";
private async Task Initialize() private async Task Initialize()
{ {
var pq = new PaginationQuery() { Limit = 10 }; var pq = new PaginationQuery() { Limit = 30 };
var res = await ApiService.Timelines.GetHomeTimeline(pq); var res = await ApiService.Timelines.GetHomeTimeline(pq);
MaxId = res[0].Id; MaxId = res[0].Id;
MinId = res.Last().Id; MinId = res.Last().Id;
@ -47,22 +36,16 @@
{ {
if (LockFetch) return; if (LockFetch) return;
LockFetch = true; LockFetch = true;
var pq = new PaginationQuery() { Limit = 5, MaxId = MinId }; var pq = new PaginationQuery() { Limit = 10, MaxId = MinId };
var res = await ApiService.Timelines.GetHomeTimeline(pq); var res = await ApiService.Timelines.GetHomeTimeline(pq);
if (res.Count > 0) if (res.Count > 0)
{ {
MinId = res.Last().Id; MinId = res.Last().Id;
Timeline.AddRange(res); Timeline.AddRange(res);
StateHasChanged();
} }
LockFetch = false; LockFetch = false;
} }
private void OnEnd(IntersectionObserverEntry entry)
{
FetchOlder();
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)

View file

@ -1,11 +0,0 @@
.scroller {
display: flex;
flex-direction: column;
overflow-y: scroll;
overflow-x: clip;
max-height: 100vh;
width: 100%;
align-items: center;
overflow-anchor: none;
overscroll-behavior: contain;
}

View file

@ -0,0 +1,235 @@
@using System.Collections.Specialized
@using System.Runtime.InteropServices.JavaScript
@using Iceshrimp.Shared.Schemas
@using Ljbc1994.Blazor.IntersectionObserver
@using Ljbc1994.Blazor.IntersectionObserver.API
@inject IIntersectionObserverService ObserverService
@inject IJSRuntime Js
<div @ref="@Scroller" class="scroller">
<div @ref="@padTopRef" class="padding top" style="height: @(PadTop + "px")"></div>
@foreach (var el in RenderedList)
{
<div class="target" @ref="@Ref">
<TimelineNote @key="el.Id" Note="el"></TimelineNote>
</div>
}
<div @ref="@padBotRef" class="padding bottom" style="height: @(PadBottom + "px")"></div>
</div>
@code {
[Parameter, EditorRequired] public required List<NoteResponse> NoteResponseList { get; set; }
[Parameter, EditorRequired] public required EventCallback ReachedEnd { get; set; }
[Parameter, EditorRequired] public required EventCallback ReachedStart { get; set; }
private List<NoteResponse> RenderedList { get; set; } = new();
private Dictionary<string, int> Height { get; set; } = new();
private string MaxId { get; set; }
private string MinId { get; set; }
private int RenderIndex { get; set; }
private int PadTop { get; set; } = 0;
private int PadBottom { get; set; } = 0;
private int UpdateCount { get; set; } = 5;
private int _count = 15;
private List<ElementReference> _refs = [];
private IntersectionObserver? ObserverTop { get; set; }
private IntersectionObserver? ObserverBottom { get; set; }
private IntersectionObserver? OvrscrlObsvTop { get; set; }
private IntersectionObserver? OvrscrlObsvBottom { get; set; }
private bool _overscrollTop = false;
private bool _overscrollBottom = false;
private ElementReference padTopRef;
private ElementReference padBotRef;
private ElementReference Scroller;
private ElementReference Ref
{
set => _refs.Add(value);
}
private bool interlock = false;
private IJSObjectReference Module { get; set; }
private void InitialRender(string? id)
{
var a = new List<NoteResponse>();
if (id != null)
{
RenderIndex = NoteResponseList.IndexOf(NoteResponseList.First(el => el.Id == id));
a = NoteResponseList.GetRange(RenderIndex, RenderIndex + _count);
}
else
{
a = NoteResponseList.GetRange(0, _count);
}
RenderedList = a;
}
private async Task LoadMore()
{
await ReachedEnd.InvokeAsync();
}
private async Task RemoveAbove(int amount)
{
for (int i = 0; i < amount; i++)
{
var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]);
PadTop += height;
Height[RenderedList[i].Id] = height;
}
RenderedList.RemoveRange(0, amount);
}
private async Task Down()
{
if (OvrscrlObsvBottom is null) throw new Exception("Tried to use observer that does not exist");
await OvrscrlObsvBottom.Disconnect();
var index = NoteResponseList.IndexOf(RenderedList.Last());
Console.WriteLine($"Index: {index}");
if (index >= NoteResponseList.Count - 1)
{
Console.WriteLine("end of data, requesting more");
await LoadMore();
}
else
{
var a = NoteResponseList.GetRange(index + 1, 5);
var heightChange = 0;
foreach (var el in a)
{
if (Height.ContainsKey(el.Id))
{
heightChange += Height[el.Id];
Console.WriteLine("found height");
}
else
{
Console.WriteLine("Did not find height");
}
}
if (PadBottom > 0) PadBottom -= heightChange;
Console.WriteLine($"Pad bottom height: {PadBottom}");
RenderedList.AddRange(a);
await RemoveAbove(UpdateCount);
interlock = false;
StateHasChanged();
}
await OvrscrlObsvBottom.Observe(padBotRef);
}
private async Task Up()
{
if (OvrscrlObsvTop is null) throw new Exception("Tried to use observer that does not exist");
await OvrscrlObsvTop.Disconnect();
for (int i = 0; i < UpdateCount; i++)
{
var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]);
PadBottom += height;
Height[RenderedList[i].Id] = height;
}
var index = NoteResponseList.IndexOf(RenderedList.First());
var a = NoteResponseList.GetRange(index - UpdateCount, UpdateCount);
var heightChange = 0;
foreach (var el in a)
{
heightChange += Height[el.Id];
}
PadTop -= heightChange;
RenderedList.InsertRange(0, a);
RenderedList.RemoveRange(RenderedList.Count - UpdateCount, UpdateCount);
StateHasChanged();
interlock = false;
await OvrscrlObsvTop.Observe(padTopRef);
}
private async Task SetupObservers()
{
var options = new IntersectionObserverOptions
{
RootMargin = "100%"
};
OvrscrlObsvTop = await ObserverService.Create(OverscrollCallbackTop);
OvrscrlObsvBottom = await ObserverService.Create(OverscrollCallbackBottom);
await OvrscrlObsvTop.Observe(padTopRef);
await OvrscrlObsvBottom.Observe(padBotRef);
}
private async void OverscrollCallbackTop(IList<IntersectionObserverEntry> list)
{
Console.WriteLine("Top callback fired");
var entry = list.First();
_overscrollTop = entry.IsIntersecting;
if (interlock == false)
{
var index = NoteResponseList.IndexOf(RenderedList.First());
if (index == 0)
{
Console.WriteLine("Can't go up further");
return;
}
interlock = true;
Console.WriteLine("first observed");
if (list.First().IsIntersecting)
{
Console.WriteLine("Shifting up");
await Up();
}
interlock = false;
}
}
private async void OverscrollCallbackBottom(IList<IntersectionObserverEntry> list)
{
Console.WriteLine("Bottom callback fired");
var entry = list.First();
_overscrollBottom = entry.IsIntersecting;
if (interlock == false)
{
interlock = true;
Console.WriteLine("last observerd");
if (list.First().IsIntersecting)
{
Console.WriteLine("Shifting down");
await Down();
}
interlock = false;
}
}
protected override async Task OnInitializedAsync()
{
InitialRender(null);
}
// protected override Task OnParametersSetAsync()
// {
// return;
// }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Module = await Js.InvokeAsync<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js");
await SetupObservers();
}
}
}

View file

@ -0,0 +1,19 @@
.padding {
width: 1px;
}
.scroller {
display: flex;
flex-direction: column;
overflow-y: scroll;
overflow-x: clip;
width: 100%;
align-items: center;
overflow-anchor: none;
overscroll-behavior: contain;
}
.target {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
}

View file

@ -0,0 +1,8 @@
export function GetHeight(ref){
if (ref != null) {
console.log(ref.scrollHeight)
return ref.scrollHeight;
} else {
return 0;
}
}