Iceshrimp.NET/Iceshrimp.Frontend/Components/VirtualScroller.razor
2024-06-29 00:48:53 +02:00

280 lines
No EOL
9 KiB
Text

@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
<div @ref="@_scroller" class="scroller">
<div @ref="@_padTopRef" class="padding top" style="height: @(State.PadTop + "px")"></div>
@if (_loadingTop)
{
<div class="target">
Loading!
</div>
}
@foreach (var el in State.RenderedList)
{
<div class="target" @ref="@Ref">
<TimelineNote @key="el.Id" Note="el"></TimelineNote>
</div>
}
@if (_loadingBottom)
{
<div class="target">
Loading!
</div>
}
else
{
<div class="target">
<span>The end!<button @onclick="Down">Load more!</button></span>
</div>
}
<div @ref="@_padBotRef" class="padding bottom" style="height: @(State.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 VirtualScrollerState State { get; set; } = new();
private int UpdateCount { get; set; } = 15;
private int _count = 30;
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 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);
}
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 (var i = 0; i < amount; i++)
{
var height = await Module.InvokeAsync<int>("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<int>("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<IntersectionObserverEntry> 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<IntersectionObserverEntry> 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<float>("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<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js");
await SetupObservers();
}
if (_setScroll)
{
await SetScrollTop();
_setScroll = false;
}
await SaveState();
}
}