using Iceshrimp.Frontend.Core.Services; using Iceshrimp.Frontend.Core.Services.StateServicePatterns; using Iceshrimp.Shared.Schemas.Web; using Ljbc1994.Blazor.IntersectionObserver; using Ljbc1994.Blazor.IntersectionObserver.API; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; namespace Iceshrimp.Frontend.Components; public partial class VirtualScroller : IAsyncDisposable { [Inject] private IIntersectionObserverService ObserverService { get; set; } = null!; [Inject] private IJSRuntime Js { get; set; } = null!; [Inject] private StateService StateService { get; set; } = null!; [Parameter] [EditorRequired] public required List NoteResponseList { get; set; } [Parameter] [EditorRequired] public required Func> 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? 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; } = null!; private void InitialRender(string? id) { State.RenderedList = NoteResponseList.Count < _count ? NoteResponseList : NoteResponseList.GetRange(0, _count); } public async ValueTask DisposeAsync() { await SaveState(); } private async Task LoadOlder() { _loadingBottom = true; StateHasChanged(); var moreAvailable = await ReachedEnd(); if (moreAvailable == false) { if (OvrscrlObsvBottom is null) throw new Exception("Tried to use observer that does not exist"); await OvrscrlObsvBottom.Disconnect(); } _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 (var 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.TryGetValue(el.Id, out var value)) heightChange += value; } 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(int updateCount) { if (OvrscrlObsvTop is null) throw new Exception("Tried to use observer that does not exist"); await OvrscrlObsvTop.Disconnect(); for (var 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) { State.Height.TryGetValue(el.Id, out var height); heightChange += height; } 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() { OvrscrlObsvTop = await ObserverService.Create(OverscrollCallbackTop); OvrscrlObsvBottom = await ObserverService.Create(OverscrollCallbackBottom); await OvrscrlObsvTop.Observe(_padTopRef); await OvrscrlObsvBottom.Observe(_padBotRef); } public async Task OnNewNote() { if (_overscrollTop && _interlock == false) { _interlock = true; await Up(1); } } 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; } var updateCount = UpdateCount; if (index < UpdateCount) { updateCount = index; } _interlock = true; if (list.First().IsIntersecting) { await Up(updateCount); } _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 void OnInitialized() { 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; } } }