@using System.Collections.Specialized @using System.Runtime.InteropServices.JavaScript @using Iceshrimp.Frontend.Core.Miscellaneous @using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services.StateServicePatterns @using Iceshrimp.Shared.Schemas @using Ljbc1994.Blazor.IntersectionObserver @using Ljbc1994.Blazor.IntersectionObserver.API @inject IIntersectionObserverService ObserverService @inject IJSRuntime Js @inject StateService StateService
@if(loadingTop){
Loading!
} @foreach (var el in State.RenderedList) {
} @if(loadingBottom){
Loading!
}
@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 VirtualScrollerState State { get; set; } = new(); private int UpdateCount { get; set; } = 15; private int _count = 30; 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 bool loadingTop = false; private bool loadingBottom = false; private bool setScroll = false; 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(); a = NoteResponseList.GetRange(0, _count); State.RenderedList = a; } private async Task LoadOlder() { loadingBottom = true; StateHasChanged(); await ReachedEnd.InvokeAsync(); loadingBottom = false; StateHasChanged(); } private async Task LoadNewer() { loadingTop = true; StateHasChanged(); await ReachedStart.InvokeAsync(); loadingTop = false; StateHasChanged(); } private async Task SaveState() { await GetScrollTop(); StateService.VirtualScroller.SetState("home", State); } private async Task RemoveAbove(int amount) { for (int i = 0; i < amount; i++) { var height = await Module.InvokeAsync("GetHeight", _refs[i]); State.PadTop += height; State.Height[State.RenderedList[i].Id] = height; } State.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(State.RenderedList.Last()); Console.WriteLine($"Index: {index}"); if (index >= NoteResponseList.Count - (1 + UpdateCount)) { Console.WriteLine("end of data, requesting more"); await LoadOlder(); } else { var a = NoteResponseList.GetRange(index + 1, UpdateCount); var heightChange = 0; foreach (var el in a) { if (State.Height.ContainsKey(el.Id)) { heightChange += State.Height[el.Id]; Console.WriteLine("found height"); } else { Console.WriteLine("Did not find height"); } } if (State.PadBottom > 0) State.PadBottom -= heightChange; Console.WriteLine($"Pad bottom height: {State.PadBottom}"); State.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]); State.PadBottom += height; State.Height[State.RenderedList[i].Id] = height; } var index = NoteResponseList.IndexOf(State.RenderedList.First()); var a = NoteResponseList.GetRange(index - UpdateCount, UpdateCount); var heightChange = 0; foreach (var el in a) { heightChange += State.Height[el.Id]; } State.PadTop -= heightChange; State.RenderedList.InsertRange(0, a); State.RenderedList.RemoveRange(State.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(State.RenderedList.First()); if (index == 0) { Console.WriteLine("Can't go up further"); await LoadNewer(); 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; } } private async Task GetScrollTop() { var scrollTop = await Module.InvokeAsync("GetScrollTop", Scroller); State.ScrollTop = scrollTop; } private async Task SetScrollTop() { await Module.InvokeVoidAsync("SetScrollTop", State.ScrollTop, Scroller); } protected override async Task OnInitializedAsync() { try { var virtualScrollerState = StateService.VirtualScroller.GetState("home"); State = virtualScrollerState; setScroll = true; } catch (ArgumentException) { InitialRender(null); } } protected override void OnParametersSet() { StateHasChanged(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { Module = await Js.InvokeAsync("import", "./Components/VirtualScroller.razor.js"); await SetupObservers(); } if (setScroll) { await SetScrollTop(); setScroll = false; } await SaveState(); } }