From 004ddedde3f005ba79dc51d81bfba2e7c302da01 Mon Sep 17 00:00:00 2001 From: Lilian Date: Sat, 15 Jun 2024 21:04:37 +0200 Subject: [PATCH] [frontend] Basic state service and state support in Timeline --- .../Components/TimelineComponent.razor | 65 +++++-- .../Components/VirtualScroller.razor | 180 +++++++++++------- .../Components/VirtualScroller.razor.js | 10 +- .../Core/Services/StateService.cs | 11 ++ .../StateServicePatterns/TimelineState.cs | 25 +++ .../VirtualScrollerState.cs | 29 +++ Iceshrimp.Frontend/Startup.cs | 1 + 7 files changed, 240 insertions(+), 81 deletions(-) create mode 100644 Iceshrimp.Frontend/Core/Services/StateService.cs create mode 100644 Iceshrimp.Frontend/Core/Services/StateServicePatterns/TimelineState.cs create mode 100644 Iceshrimp.Frontend/Core/Services/StateServicePatterns/VirtualScrollerState.cs diff --git a/Iceshrimp.Frontend/Components/TimelineComponent.razor b/Iceshrimp.Frontend/Components/TimelineComponent.razor index 4bb226ea..200acedd 100644 --- a/Iceshrimp.Frontend/Components/TimelineComponent.razor +++ b/Iceshrimp.Frontend/Components/TimelineComponent.razor @@ -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) { - + } else {
Loading
} -@code { - private List 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); } } diff --git a/Iceshrimp.Frontend/Components/VirtualScroller.razor b/Iceshrimp.Frontend/Components/VirtualScroller.razor index c7cd2961..c891bc99 100644 --- a/Iceshrimp.Frontend/Components/VirtualScroller.razor +++ b/Iceshrimp.Frontend/Components/VirtualScroller.razor @@ -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
-
- @foreach (var el in RenderedList) +
+ @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 List RenderedList { get; set; } = new(); - private Dictionary 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 _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 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; + private bool setScroll = false; private ElementReference Ref { @@ -52,35 +64,45 @@ private void InitialRender(string? id) { var a = new List(); - 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("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("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("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("import", "./Components/VirtualScroller.razor.js"); await SetupObservers(); } + + if (setScroll) + { + await SetScrollTop(); + setScroll = false; + } + + await SaveState(); } } \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/VirtualScroller.razor.js b/Iceshrimp.Frontend/Components/VirtualScroller.razor.js index 3145f4bc..954d8cb9 100644 --- a/Iceshrimp.Frontend/Components/VirtualScroller.razor.js +++ b/Iceshrimp.Frontend/Components/VirtualScroller.razor.js @@ -5,4 +5,12 @@ export function GetHeight(ref){ } else { return 0; } -} \ No newline at end of file +} + +export function GetScrollTop(ref) { + return ref.scrollTop; +} + +export function SetScrollTop(number, ref) { + ref.scrollTop = number; +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Services/StateService.cs b/Iceshrimp.Frontend/Core/Services/StateService.cs new file mode 100644 index 00000000..80c1bb88 --- /dev/null +++ b/Iceshrimp.Frontend/Core/Services/StateService.cs @@ -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(); + +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Services/StateServicePatterns/TimelineState.cs b/Iceshrimp.Frontend/Core/Services/StateServicePatterns/TimelineState.cs new file mode 100644 index 00000000..d9d0f6da --- /dev/null +++ b/Iceshrimp.Frontend/Core/Services/StateServicePatterns/TimelineState.cs @@ -0,0 +1,25 @@ +using Iceshrimp.Shared.Schemas; + +namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns; + +public class Timeline +{ + private Dictionary 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 Timeline = []; + public required string? MaxId; + public required string? MinId; +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Services/StateServicePatterns/VirtualScrollerState.cs b/Iceshrimp.Frontend/Core/Services/StateServicePatterns/VirtualScrollerState.cs new file mode 100644 index 00000000..de308cef --- /dev/null +++ b/Iceshrimp.Frontend/Core/Services/StateServicePatterns/VirtualScrollerState.cs @@ -0,0 +1,29 @@ +using Iceshrimp.Shared.Schemas; + +namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns; + +public class VirtualScroller +{ + private Dictionary 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 RenderedList = []; + public Dictionary Height = new(); + public int PadTop = 0; + public int PadBottom = 0; + public float ScrollTop = 0; + +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Startup.cs b/Iceshrimp.Frontend/Startup.cs index 7ec6ece6..d3dbd38f 100644 --- a/Iceshrimp.Frontend/Startup.cs +++ b/Iceshrimp.Frontend/Startup.cs @@ -21,6 +21,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddBlazoredLocalStorageAsSingleton();