[frontend] Basic state service and state support in Timeline

This commit is contained in:
Lilian 2024-06-15 21:04:37 +02:00
parent 9dbbbfcc53
commit 004ddedde3
No known key found for this signature in database
GPG key ID: 007CA12D692829E1
7 changed files with 240 additions and 81 deletions

View file

@ -1,47 +1,66 @@
@using Iceshrimp.Frontend.Core.Miscellaneous @using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@using Ljbc1994.Blazor.IntersectionObserver @using Ljbc1994.Blazor.IntersectionObserver
@using Ljbc1994.Blazor.IntersectionObserver.API @using Ljbc1994.Blazor.IntersectionObserver.API
@using Ljbc1994.Blazor.IntersectionObserver.Components @using Ljbc1994.Blazor.IntersectionObserver.Components
@inject ApiService ApiService @inject ApiService ApiService
@inject StateService StateService
@if (_init) @if (_init)
{ {
<VirtualScroller NoteResponseList="Timeline" ReachedEnd="FetchOlder" ReachedStart="FetchOlder" /> <VirtualScroller NoteResponseList="State.Timeline" ReachedEnd="FetchOlder" ReachedStart="FetchNewer" />
} }
else else
{ {
<div>Loading</div> <div>Loading</div>
} }
@code { @code {
private List<NoteResponse> Timeline { get; set; } = []; private TimelineState State { get; set; } = new()
private bool _init = false; {
private string? MaxId { get; set; } Timeline = [],
private string? MinId { get; set; } MaxId = null,
private bool LockFetch { get; set; } MinId = null
};
private bool _init = false;
private bool LockFetch { get; set; }
private async Task Initialize() private async Task Initialize()
{ {
var pq = new PaginationQuery() { Limit = 30 }; var pq = new PaginationQuery() { Limit = 30 };
var res = await ApiService.Timelines.GetHomeTimeline(pq); var res = await ApiService.Timelines.GetHomeTimeline(pq);
MaxId = res[0].Id; State.MaxId = res[0].Id;
MinId = res.Last().Id; State.MinId = res.Last().Id;
Timeline = res; State.Timeline = res;
} }
private async Task FetchOlder() private async Task FetchOlder()
{ {
if (LockFetch) return; if (LockFetch) return;
LockFetch = true; LockFetch = true;
var pq = new PaginationQuery() { Limit = 10, MaxId = MinId }; var pq = new PaginationQuery() { Limit = 15, MaxId = State.MinId };
var res = await ApiService.Timelines.GetHomeTimeline(pq); var res = await ApiService.Timelines.GetHomeTimeline(pq);
if (res.Count > 0) if (res.Count > 0)
{ {
MinId = res.Last().Id; State.MinId = res.Last().Id;
Timeline.AddRange(res); State.Timeline.AddRange(res);
}
LockFetch = false;
}
private async Task FetchNewer()
{
if (LockFetch) return;
LockFetch = true;
var pq = new PaginationQuery() { Limit = 15, MinId = State.MaxId };
var res = await ApiService.Timelines.GetHomeTimeline(pq);
if (res.Count > 0)
{
State.MinId = res.Last().Id;
State.Timeline.InsertRange(0, res);
} }
LockFetch = false; LockFetch = false;
} }
@ -50,10 +69,24 @@
{ {
if (firstRender) if (firstRender)
{ {
await Initialize(); try
_init = true; {
StateHasChanged(); var timeline = StateService.Timeline.GetState("home");
State = timeline;
_init = true;
StateHasChanged();
}
catch (ArgumentException)
{
await Initialize();
_init = true;
StateHasChanged();
}
} }
StateService.Timeline.SetState("home", State);
} }
} }

View file

@ -1,45 +1,57 @@
@using System.Collections.Specialized @using System.Collections.Specialized
@using System.Runtime.InteropServices.JavaScript @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 Iceshrimp.Shared.Schemas
@using Ljbc1994.Blazor.IntersectionObserver @using Ljbc1994.Blazor.IntersectionObserver
@using Ljbc1994.Blazor.IntersectionObserver.API @using Ljbc1994.Blazor.IntersectionObserver.API
@inject IIntersectionObserverService ObserverService @inject IIntersectionObserverService ObserverService
@inject IJSRuntime Js @inject IJSRuntime Js
@inject StateService StateService
<div @ref="@Scroller" class="scroller"> <div @ref="@Scroller" class="scroller">
<div @ref="@padTopRef" class="padding top" style="height: @(PadTop + "px")"></div> <div @ref="@padTopRef" class="padding top" style="height: @(State.PadTop + "px")"></div>
@foreach (var el in RenderedList) @if(loadingTop){
<div class="target">
Loading!
</div>
}
@foreach (var el in State.RenderedList)
{ {
<div class="target" @ref="@Ref"> <div class="target" @ref="@Ref">
<TimelineNote @key="el.Id" Note="el"></TimelineNote> <TimelineNote @key="el.Id" Note="el"></TimelineNote>
</div> </div>
} }
<div @ref="@padBotRef" class="padding bottom" style="height: @(PadBottom + "px")"></div> @if(loadingBottom){
<div class="target">
Loading!
</div>
}
<div @ref="@padBotRef" class="padding bottom" style="height: @(State.PadBottom + "px")"></div>
</div> </div>
@code { @code {
[Parameter, EditorRequired] public required List<NoteResponse> NoteResponseList { get; set; } [Parameter, EditorRequired] public required List<NoteResponse> NoteResponseList { get; set; }
[Parameter, EditorRequired] public required EventCallback ReachedEnd { get; set; } [Parameter, EditorRequired] public required EventCallback ReachedEnd { get; set; }
[Parameter, EditorRequired] public required EventCallback ReachedStart { get; set; } [Parameter, EditorRequired] public required EventCallback ReachedStart { get; set; }
private List<NoteResponse> RenderedList { get; set; } = new(); private VirtualScrollerState State { get; set; } = new();
private Dictionary<string, int> Height { get; set; } = new(); private int UpdateCount { get; set; } = 15;
private string MaxId { get; set; } private int _count = 30;
private string MinId { get; set; } private List<ElementReference> _refs = [];
private int RenderIndex { get; set; } private IntersectionObserver? ObserverTop { get; set; }
private int PadTop { get; set; } = 0; private IntersectionObserver? ObserverBottom { get; set; }
private int PadBottom { get; set; } = 0; private IntersectionObserver? OvrscrlObsvTop { get; set; }
private int UpdateCount { get; set; } = 5; private IntersectionObserver? OvrscrlObsvBottom { get; set; }
private int _count = 15; private bool _overscrollTop = false;
private List<ElementReference> _refs = []; private bool _overscrollBottom = false;
private IntersectionObserver? ObserverTop { get; set; } private ElementReference padTopRef;
private IntersectionObserver? ObserverBottom { get; set; } private ElementReference padBotRef;
private IntersectionObserver? OvrscrlObsvTop { get; set; } private ElementReference Scroller;
private IntersectionObserver? OvrscrlObsvBottom { get; set; } private bool loadingTop = false;
private bool _overscrollTop = false; private bool loadingBottom = false;
private bool _overscrollBottom = false; private bool setScroll = false;
private ElementReference padTopRef;
private ElementReference padBotRef;
private ElementReference Scroller;
private ElementReference Ref private ElementReference Ref
{ {
@ -52,35 +64,45 @@
private void InitialRender(string? id) private void InitialRender(string? id)
{ {
var a = new List<NoteResponse>(); var a = new List<NoteResponse>();
if (id != null) a = NoteResponseList.GetRange(0, _count);
{ State.RenderedList = a;
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() private async Task LoadOlder()
{ {
loadingBottom = true;
StateHasChanged();
await ReachedEnd.InvokeAsync(); 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) private async Task RemoveAbove(int amount)
{ {
for (int i = 0; i < amount; i++) for (int i = 0; i < amount; i++)
{ {
var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]); var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]);
PadTop += height; State.PadTop += height;
Height[RenderedList[i].Id] = height; State.Height[State.RenderedList[i].Id] = height;
} }
RenderedList.RemoveRange(0, amount); State.RenderedList.RemoveRange(0, amount);
} }
@ -89,22 +111,22 @@
if (OvrscrlObsvBottom is null) throw new Exception("Tried to use observer that does not exist"); if (OvrscrlObsvBottom is null) throw new Exception("Tried to use observer that does not exist");
await OvrscrlObsvBottom.Disconnect(); await OvrscrlObsvBottom.Disconnect();
var index = NoteResponseList.IndexOf(RenderedList.Last()); var index = NoteResponseList.IndexOf(State.RenderedList.Last());
Console.WriteLine($"Index: {index}"); Console.WriteLine($"Index: {index}");
if (index >= NoteResponseList.Count - 1) if (index >= NoteResponseList.Count - (1 + UpdateCount))
{ {
Console.WriteLine("end of data, requesting more"); Console.WriteLine("end of data, requesting more");
await LoadMore(); await LoadOlder();
} }
else else
{ {
var a = NoteResponseList.GetRange(index + 1, 5); var a = NoteResponseList.GetRange(index + 1, UpdateCount);
var heightChange = 0; var heightChange = 0;
foreach (var el in a) foreach (var el in a)
{ {
if (Height.ContainsKey(el.Id)) if (State.Height.ContainsKey(el.Id))
{ {
heightChange += Height[el.Id]; heightChange += State.Height[el.Id];
Console.WriteLine("found height"); Console.WriteLine("found height");
} }
else else
@ -113,10 +135,10 @@
} }
} }
if (PadBottom > 0) PadBottom -= heightChange; if (State.PadBottom > 0) State.PadBottom -= heightChange;
Console.WriteLine($"Pad bottom height: {PadBottom}"); Console.WriteLine($"Pad bottom height: {State.PadBottom}");
RenderedList.AddRange(a); State.RenderedList.AddRange(a);
await RemoveAbove(UpdateCount); await RemoveAbove(UpdateCount);
interlock = false; interlock = false;
StateHasChanged(); StateHasChanged();
@ -132,21 +154,21 @@
for (int i = 0; i < UpdateCount; i++) for (int i = 0; i < UpdateCount; i++)
{ {
var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]); var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]);
PadBottom += height; State.PadBottom += height;
Height[RenderedList[i].Id] = height; State.Height[State.RenderedList[i].Id] = height;
} }
var index = NoteResponseList.IndexOf(RenderedList.First()); var index = NoteResponseList.IndexOf(State.RenderedList.First());
var a = NoteResponseList.GetRange(index - UpdateCount, UpdateCount); var a = NoteResponseList.GetRange(index - UpdateCount, UpdateCount);
var heightChange = 0; var heightChange = 0;
foreach (var el in a) foreach (var el in a)
{ {
heightChange += Height[el.Id]; heightChange += State.Height[el.Id];
} }
PadTop -= heightChange; State.PadTop -= heightChange;
RenderedList.InsertRange(0, a); State.RenderedList.InsertRange(0, a);
RenderedList.RemoveRange(RenderedList.Count - UpdateCount, UpdateCount); State.RenderedList.RemoveRange(State.RenderedList.Count - UpdateCount, UpdateCount);
StateHasChanged(); StateHasChanged();
interlock = false; interlock = false;
await OvrscrlObsvTop.Observe(padTopRef); await OvrscrlObsvTop.Observe(padTopRef);
@ -174,10 +196,11 @@
if (interlock == false) if (interlock == false)
{ {
var index = NoteResponseList.IndexOf(RenderedList.First()); var index = NoteResponseList.IndexOf(State.RenderedList.First());
if (index == 0) if (index == 0)
{ {
Console.WriteLine("Can't go up further"); Console.WriteLine("Can't go up further");
await LoadNewer();
return; return;
} }
@ -213,15 +236,36 @@
} }
} }
protected override async Task OnInitializedAsync() private async Task GetScrollTop()
{ {
InitialRender(null); var scrollTop = await Module.InvokeAsync<float>("GetScrollTop", Scroller);
State.ScrollTop = scrollTop;
} }
// protected override Task OnParametersSetAsync() private async Task SetScrollTop()
// { {
// return; 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) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@ -230,6 +274,14 @@
Module = await Js.InvokeAsync<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js"); Module = await Js.InvokeAsync<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js");
await SetupObservers(); await SetupObservers();
} }
if (setScroll)
{
await SetScrollTop();
setScroll = false;
}
await SaveState();
} }
} }

View file

@ -5,4 +5,12 @@ export function GetHeight(ref){
} else { } else {
return 0; return 0;
} }
} }
export function GetScrollTop(ref) {
return ref.scrollTop;
}
export function SetScrollTop(number, ref) {
ref.scrollTop = number;
}

View file

@ -0,0 +1,11 @@
using Iceshrimp.Frontend.Core.Services.StateServicePatterns;
namespace Iceshrimp.Frontend.Core.Services;
public class StateService
{
public VirtualScroller VirtualScroller { get; } = new();
public Timeline Timeline { get; } = new();
}

View file

@ -0,0 +1,25 @@
using Iceshrimp.Shared.Schemas;
namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns;
public class Timeline
{
private Dictionary<string, TimelineState> States { get; } = new();
public void SetState(string id, TimelineState state)
{
States[id] = state;
}
public TimelineState GetState(string id)
{
States.TryGetValue(id, out var state);
return state ?? throw new ArgumentException($"Requested state '{id}' does not exist.");
}
}
public class TimelineState
{
public required List<NoteResponse> Timeline = [];
public required string? MaxId;
public required string? MinId;
}

View file

@ -0,0 +1,29 @@
using Iceshrimp.Shared.Schemas;
namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns;
public class VirtualScroller
{
private Dictionary<string, VirtualScrollerState> States { get; } = new();
public void SetState(string id, VirtualScrollerState state)
{
States[id] = state;
}
public VirtualScrollerState GetState(string id)
{
States.TryGetValue(id, out var state);
return state ?? throw new ArgumentException($"Requested state '{id}' does not exist.");
}
}
public class VirtualScrollerState
{
public List<NoteResponse> RenderedList = [];
public Dictionary<string, int> Height = new();
public int PadTop = 0;
public int PadBottom = 0;
public float ScrollTop = 0;
}

View file

@ -21,6 +21,7 @@ builder.Services.AddSingleton<SessionService>();
builder.Services.AddSingleton<StreamingService>(); builder.Services.AddSingleton<StreamingService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddSingleton<ComposeService>(); builder.Services.AddSingleton<ComposeService>();
builder.Services.AddSingleton<StateService>();
builder.Services.AddAuthorizationCore(); builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddCascadingAuthenticationState();
builder.Services.AddBlazoredLocalStorageAsSingleton(); builder.Services.AddBlazoredLocalStorageAsSingleton();