@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; public 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.Count < _count ? NoteResponseList : 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()); if (index >= NoteResponseList.Count - (1 + UpdateCount)) { 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]; } else { } } if (State.PadBottom > 0) State.PadBottom -= heightChange; 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() { // Enabling root margin causes erratic virtual scroller behavior, i've not figured out why 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) { var entry = list.First(); _overscrollTop = entry.IsIntersecting; if (_interlock == false) { var index = NoteResponseList.IndexOf(State.RenderedList.First()); if (index == 0) { await LoadNewer(); return; } _interlock = true; if (list.First().IsIntersecting) { await Up(); } _interlock = false; } } private async void OverscrollCallbackBottom(IList list) { var entry = list.First(); _overscrollBottom = entry.IsIntersecting; if (_interlock == false) { _interlock = true; if (list.First().IsIntersecting) { 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(); } }