[frontend] Basic state service and state support in Timeline
This commit is contained in:
parent
9dbbbfcc53
commit
004ddedde3
7 changed files with 240 additions and 81 deletions
|
@ -1,47 +1,66 @@
|
|||
@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
|
||||
@using Ljbc1994.Blazor.IntersectionObserver.Components
|
||||
@inject ApiService ApiService
|
||||
@inject StateService StateService
|
||||
|
||||
|
||||
@if (_init)
|
||||
{
|
||||
<VirtualScroller NoteResponseList="Timeline" ReachedEnd="FetchOlder" ReachedStart="FetchOlder" />
|
||||
<VirtualScroller NoteResponseList="State.Timeline" ReachedEnd="FetchOlder" ReachedStart="FetchNewer" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>Loading</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<NoteResponse> Timeline { get; set; } = [];
|
||||
private bool _init = false;
|
||||
private string? MaxId { get; set; }
|
||||
private string? MinId { get; set; }
|
||||
private bool LockFetch { get; set; }
|
||||
@code {
|
||||
private TimelineState State { get; set; } = new()
|
||||
{
|
||||
Timeline = [],
|
||||
MaxId = null,
|
||||
MinId = null
|
||||
};
|
||||
private bool _init = false;
|
||||
private bool LockFetch { get; set; }
|
||||
|
||||
private async Task Initialize()
|
||||
{
|
||||
var pq = new PaginationQuery() { Limit = 30 };
|
||||
var res = await ApiService.Timelines.GetHomeTimeline(pq);
|
||||
MaxId = res[0].Id;
|
||||
MinId = res.Last().Id;
|
||||
Timeline = res;
|
||||
State.MaxId = res[0].Id;
|
||||
State.MinId = res.Last().Id;
|
||||
State.Timeline = res;
|
||||
}
|
||||
|
||||
private async Task FetchOlder()
|
||||
{
|
||||
if (LockFetch) return;
|
||||
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);
|
||||
if (res.Count > 0)
|
||||
{
|
||||
MinId = res.Last().Id;
|
||||
Timeline.AddRange(res);
|
||||
State.MinId = res.Last().Id;
|
||||
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;
|
||||
}
|
||||
|
@ -50,10 +69,24 @@
|
|||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Initialize();
|
||||
_init = true;
|
||||
StateHasChanged();
|
||||
try
|
||||
{
|
||||
var timeline = StateService.Timeline.GetState("home");
|
||||
State = timeline;
|
||||
_init = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
await Initialize();
|
||||
_init = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
StateService.Timeline.SetState("home", State);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,45 +1,57 @@
|
|||
@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
|
||||
|
||||
<div @ref="@Scroller" class="scroller">
|
||||
<div @ref="@padTopRef" class="padding top" style="height: @(PadTop + "px")"></div>
|
||||
@foreach (var el in RenderedList)
|
||||
<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>
|
||||
}
|
||||
<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>
|
||||
|
||||
@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 List<NoteResponse> RenderedList { get; set; } = new();
|
||||
private Dictionary<string, int> Height { get; set; } = new();
|
||||
private string MaxId { get; set; }
|
||||
private string MinId { get; set; }
|
||||
private int RenderIndex { get; set; }
|
||||
private int PadTop { get; set; } = 0;
|
||||
private int PadBottom { get; set; } = 0;
|
||||
private int UpdateCount { get; set; } = 5;
|
||||
private int _count = 15;
|
||||
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;
|
||||
[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
|
||||
{
|
||||
|
@ -52,35 +64,45 @@
|
|||
private void InitialRender(string? id)
|
||||
{
|
||||
var a = new List<NoteResponse>();
|
||||
if (id != null)
|
||||
{
|
||||
RenderIndex = NoteResponseList.IndexOf(NoteResponseList.First(el => el.Id == id));
|
||||
a = NoteResponseList.GetRange(RenderIndex, RenderIndex + _count);
|
||||
}
|
||||
else
|
||||
{
|
||||
a = NoteResponseList.GetRange(0, _count);
|
||||
}
|
||||
|
||||
|
||||
RenderedList = a;
|
||||
a = NoteResponseList.GetRange(0, _count);
|
||||
State.RenderedList = a;
|
||||
}
|
||||
|
||||
private async Task LoadMore()
|
||||
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<int>("GetHeight", _refs[i]);
|
||||
PadTop += height;
|
||||
Height[RenderedList[i].Id] = height;
|
||||
State.PadTop += 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");
|
||||
await OvrscrlObsvBottom.Disconnect();
|
||||
|
||||
var index = NoteResponseList.IndexOf(RenderedList.Last());
|
||||
var index = NoteResponseList.IndexOf(State.RenderedList.Last());
|
||||
Console.WriteLine($"Index: {index}");
|
||||
if (index >= NoteResponseList.Count - 1)
|
||||
if (index >= NoteResponseList.Count - (1 + UpdateCount))
|
||||
{
|
||||
Console.WriteLine("end of data, requesting more");
|
||||
await LoadMore();
|
||||
await LoadOlder();
|
||||
}
|
||||
else
|
||||
{
|
||||
var a = NoteResponseList.GetRange(index + 1, 5);
|
||||
var a = NoteResponseList.GetRange(index + 1, UpdateCount);
|
||||
var heightChange = 0;
|
||||
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");
|
||||
}
|
||||
else
|
||||
|
@ -113,10 +135,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (PadBottom > 0) PadBottom -= heightChange;
|
||||
Console.WriteLine($"Pad bottom height: {PadBottom}");
|
||||
if (State.PadBottom > 0) State.PadBottom -= heightChange;
|
||||
Console.WriteLine($"Pad bottom height: {State.PadBottom}");
|
||||
|
||||
RenderedList.AddRange(a);
|
||||
State.RenderedList.AddRange(a);
|
||||
await RemoveAbove(UpdateCount);
|
||||
interlock = false;
|
||||
StateHasChanged();
|
||||
|
@ -132,21 +154,21 @@
|
|||
for (int i = 0; i < UpdateCount; i++)
|
||||
{
|
||||
var height = await Module.InvokeAsync<int>("GetHeight", _refs[i]);
|
||||
PadBottom += height;
|
||||
Height[RenderedList[i].Id] = height;
|
||||
State.PadBottom += 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 heightChange = 0;
|
||||
foreach (var el in a)
|
||||
{
|
||||
heightChange += Height[el.Id];
|
||||
heightChange += State.Height[el.Id];
|
||||
}
|
||||
|
||||
PadTop -= heightChange;
|
||||
RenderedList.InsertRange(0, a);
|
||||
RenderedList.RemoveRange(RenderedList.Count - UpdateCount, UpdateCount);
|
||||
State.PadTop -= heightChange;
|
||||
State.RenderedList.InsertRange(0, a);
|
||||
State.RenderedList.RemoveRange(State.RenderedList.Count - UpdateCount, UpdateCount);
|
||||
StateHasChanged();
|
||||
interlock = false;
|
||||
await OvrscrlObsvTop.Observe(padTopRef);
|
||||
|
@ -174,10 +196,11 @@
|
|||
|
||||
if (interlock == false)
|
||||
{
|
||||
var index = NoteResponseList.IndexOf(RenderedList.First());
|
||||
var index = NoteResponseList.IndexOf(State.RenderedList.First());
|
||||
if (index == 0)
|
||||
{
|
||||
Console.WriteLine("Can't go up further");
|
||||
await LoadNewer();
|
||||
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()
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
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)
|
||||
{
|
||||
|
@ -230,6 +274,14 @@
|
|||
Module = await Js.InvokeAsync<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js");
|
||||
await SetupObservers();
|
||||
}
|
||||
|
||||
if (setScroll)
|
||||
{
|
||||
await SetScrollTop();
|
||||
setScroll = false;
|
||||
}
|
||||
|
||||
await SaveState();
|
||||
}
|
||||
|
||||
}
|
|
@ -5,4 +5,12 @@ export function GetHeight(ref){
|
|||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function GetScrollTop(ref) {
|
||||
return ref.scrollTop;
|
||||
}
|
||||
|
||||
export function SetScrollTop(number, ref) {
|
||||
ref.scrollTop = number;
|
||||
}
|
11
Iceshrimp.Frontend/Core/Services/StateService.cs
Normal file
11
Iceshrimp.Frontend/Core/Services/StateService.cs
Normal 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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -21,6 +21,7 @@ builder.Services.AddSingleton<SessionService>();
|
|||
builder.Services.AddSingleton<StreamingService>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
|
||||
builder.Services.AddSingleton<ComposeService>();
|
||||
builder.Services.AddSingleton<StateService>();
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddBlazoredLocalStorageAsSingleton();
|
||||
|
|
Loading…
Add table
Reference in a new issue