From 9dbbbfcc53b114be02ae9accd91f5a001d57cbbf Mon Sep 17 00:00:00 2001 From: Lilian Date: Sat, 8 Jun 2024 16:31:41 +0200 Subject: [PATCH] [frontend] barebones virtual scroller implementation --- .../Components/TimelineComponent.razor | 23 +- .../Components/TimelineComponent.razor.css | 11 - .../Components/VirtualScroller.razor | 235 ++++++++++++++++++ .../Components/VirtualScroller.razor.css | 19 ++ .../Components/VirtualScroller.razor.js | 8 + 5 files changed, 265 insertions(+), 31 deletions(-) create mode 100644 Iceshrimp.Frontend/Components/VirtualScroller.razor create mode 100644 Iceshrimp.Frontend/Components/VirtualScroller.razor.css create mode 100644 Iceshrimp.Frontend/Components/VirtualScroller.razor.js diff --git a/Iceshrimp.Frontend/Components/TimelineComponent.razor b/Iceshrimp.Frontend/Components/TimelineComponent.razor index 319b9382..4bb226ea 100644 --- a/Iceshrimp.Frontend/Components/TimelineComponent.razor +++ b/Iceshrimp.Frontend/Components/TimelineComponent.razor @@ -7,23 +7,14 @@ @inject ApiService ApiService -
@if (_init) { - @foreach (var note in Timeline) - { - - } - - -
END!
-
+ } else {
Loading
} -
@code { private List Timeline { get; set; } = []; @@ -31,12 +22,10 @@ private string? MaxId { get; set; } private string? MinId { get; set; } private bool LockFetch { get; set; } - public ElementReference Scroller { get; set; } - private string _rootMagin = "100%"; private async Task Initialize() { - var pq = new PaginationQuery() { Limit = 10 }; + var pq = new PaginationQuery() { Limit = 30 }; var res = await ApiService.Timelines.GetHomeTimeline(pq); MaxId = res[0].Id; MinId = res.Last().Id; @@ -47,22 +36,16 @@ { if (LockFetch) return; 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); if (res.Count > 0) { MinId = res.Last().Id; Timeline.AddRange(res); - StateHasChanged(); } LockFetch = false; } - private void OnEnd(IntersectionObserverEntry entry) - { - FetchOlder(); - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) diff --git a/Iceshrimp.Frontend/Components/TimelineComponent.razor.css b/Iceshrimp.Frontend/Components/TimelineComponent.razor.css index 6ad7460a..e69de29b 100644 --- a/Iceshrimp.Frontend/Components/TimelineComponent.razor.css +++ b/Iceshrimp.Frontend/Components/TimelineComponent.razor.css @@ -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; -} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/VirtualScroller.razor b/Iceshrimp.Frontend/Components/VirtualScroller.razor new file mode 100644 index 00000000..c7cd2961 --- /dev/null +++ b/Iceshrimp.Frontend/Components/VirtualScroller.razor @@ -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 + +
+
+ @foreach (var el in RenderedList) + { +
+ +
+ } +
+
+ +@code { + [Parameter, EditorRequired] public required List NoteResponseList { get; set; } + [Parameter, EditorRequired] public required EventCallback ReachedEnd { get; set; } + [Parameter, EditorRequired] public required EventCallback ReachedStart { get; set; } + private List RenderedList { get; set; } = new(); + private Dictionary 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 _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(); + 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("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("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 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 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("import", "./Components/VirtualScroller.razor.js"); + await SetupObservers(); + } + } + +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/VirtualScroller.razor.css b/Iceshrimp.Frontend/Components/VirtualScroller.razor.css new file mode 100644 index 00000000..10ec83f8 --- /dev/null +++ b/Iceshrimp.Frontend/Components/VirtualScroller.razor.css @@ -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; +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/VirtualScroller.razor.js b/Iceshrimp.Frontend/Components/VirtualScroller.razor.js new file mode 100644 index 00000000..3145f4bc --- /dev/null +++ b/Iceshrimp.Frontend/Components/VirtualScroller.razor.js @@ -0,0 +1,8 @@ +export function GetHeight(ref){ + if (ref != null) { + console.log(ref.scrollHeight) + return ref.scrollHeight; + } else { + return 0; + } +} \ No newline at end of file