[frontend] Add note deletion to VirtualScroller and Timeline (ISH-404)
This commit is contained in:
parent
854979b359
commit
ba42e19beb
5 changed files with 114 additions and 38 deletions
|
@ -11,12 +11,13 @@ namespace Iceshrimp.Frontend.Components;
|
||||||
public partial class VirtualScroller : IAsyncDisposable
|
public partial class VirtualScroller : IAsyncDisposable
|
||||||
{
|
{
|
||||||
[Inject] private IIntersectionObserverService ObserverService { get; set; } = null!;
|
[Inject] private IIntersectionObserverService ObserverService { get; set; } = null!;
|
||||||
[Inject] private IJSRuntime Js { get; set; } = null!;
|
[Inject] private IJSRuntime Js { get; set; } = null!;
|
||||||
[Inject] private StateService StateService { get; set; } = null!;
|
[Inject] private StateService StateService { get; set; } = null!;
|
||||||
|
[Inject] private MessageService MessageService { get; set; } = null!;
|
||||||
[Parameter] [EditorRequired] public required List<NoteResponse> NoteResponseList { get; set; }
|
[Parameter] [EditorRequired] public required List<NoteResponse> NoteResponseList { get; set; }
|
||||||
[Parameter] [EditorRequired] public required Func<Task<bool>> ReachedEnd { get; set; }
|
[Parameter] [EditorRequired] public required Func<Task<bool>> ReachedEnd { get; set; }
|
||||||
[Parameter] [EditorRequired] public required EventCallback ReachedStart { get; set; }
|
[Parameter] [EditorRequired] public required EventCallback ReachedStart { get; set; }
|
||||||
private VirtualScrollerState State { get; set; } = new();
|
private VirtualScrollerState State { get; set; } = null!;
|
||||||
private int UpdateCount { get; set; } = 15;
|
private int UpdateCount { get; set; } = 15;
|
||||||
private int _count = 30;
|
private int _count = 30;
|
||||||
private List<ElementReference> _refs = [];
|
private List<ElementReference> _refs = [];
|
||||||
|
@ -47,6 +48,7 @@ public partial class VirtualScroller : IAsyncDisposable
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await SaveState();
|
await SaveState();
|
||||||
|
MessageService.AnyNoteDeleted -= OnNoteDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadOlder()
|
private async Task LoadOlder()
|
||||||
|
@ -225,8 +227,17 @@ public partial class VirtualScroller : IAsyncDisposable
|
||||||
{
|
{
|
||||||
await Module.InvokeVoidAsync("SetScrollTop", State.ScrollTop, _scroller);
|
await Module.InvokeVoidAsync("SetScrollTop", State.ScrollTop, _scroller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnNoteDeleted(object? _, NoteResponse note)
|
||||||
|
{
|
||||||
|
State.RenderedList.Remove(note);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
|
State = StateService.VirtualScroller.CreateStateObject();
|
||||||
|
MessageService.AnyNoteDeleted += OnNoteDeleted;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var virtualScrollerState = StateService.VirtualScroller.GetState("home");
|
var virtualScrollerState = StateService.VirtualScroller.GetState("home");
|
||||||
|
@ -257,6 +268,5 @@ public partial class VirtualScroller : IAsyncDisposable
|
||||||
await SetScrollTop();
|
await SetScrollTop();
|
||||||
_setScroll = false;
|
_setScroll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ namespace Iceshrimp.Frontend.Core.Services;
|
||||||
|
|
||||||
internal class StateService(MessageService messageService)
|
internal class StateService(MessageService messageService)
|
||||||
{
|
{
|
||||||
public VirtualScroller VirtualScroller { get; } = new();
|
public VirtualScroller VirtualScroller { get; } = new(messageService);
|
||||||
public Timeline Timeline { get; } = new(messageService);
|
public Timeline Timeline { get; } = new(messageService);
|
||||||
public SingleNote SingleNote { get; } = new();
|
public SingleNote SingleNote { get; } = new();
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns;
|
||||||
internal class Timeline(MessageService messageService)
|
internal class Timeline(MessageService messageService)
|
||||||
{
|
{
|
||||||
private MessageService MessageService { get; set; } = messageService;
|
private MessageService MessageService { get; set; } = messageService;
|
||||||
private Dictionary<string, TimelineState> States { get; } = new();
|
private Dictionary<string, TimelineState> States { get; } = new();
|
||||||
|
|
||||||
public void SetState(string id, TimelineState state)
|
public void SetState(string id, TimelineState state)
|
||||||
{
|
{
|
||||||
|
@ -27,13 +27,13 @@ internal class Timeline(MessageService messageService)
|
||||||
|
|
||||||
internal class TimelineState : IDisposable
|
internal class TimelineState : IDisposable
|
||||||
{
|
{
|
||||||
private MessageService MessageService { get; set; }
|
private MessageService MessageService { get; set; }
|
||||||
public required string? MaxId;
|
public required string? MaxId;
|
||||||
public required string? MinId;
|
public required string? MinId;
|
||||||
public required List<NoteResponse> Timeline;
|
public required List<NoteResponse> Timeline;
|
||||||
|
|
||||||
[SetsRequiredMembers]
|
[SetsRequiredMembers]
|
||||||
public TimelineState(List<NoteResponse> timeline, string? maxId, string? minId, MessageService messageService)
|
internal TimelineState(List<NoteResponse> timeline, string? maxId, string? minId, MessageService messageService)
|
||||||
{
|
{
|
||||||
MaxId = maxId;
|
MaxId = maxId;
|
||||||
MinId = minId;
|
MinId = minId;
|
||||||
|
@ -54,7 +54,10 @@ internal class TimelineState : IDisposable
|
||||||
|
|
||||||
private void OnNoteDeleted(object? _, NoteResponse note)
|
private void OnNoteDeleted(object? _, NoteResponse note)
|
||||||
{
|
{
|
||||||
Timeline.Remove(note);
|
var i = Timeline.FindIndex(p => p.Id == note.Id);
|
||||||
|
if (i == 0) MaxId = Timeline[1].Id;
|
||||||
|
if (i == Timeline.Count - 1) MinId = Timeline[^2].Id;
|
||||||
|
Timeline.RemoveAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -2,10 +2,15 @@ using Iceshrimp.Shared.Schemas.Web;
|
||||||
|
|
||||||
namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns;
|
namespace Iceshrimp.Frontend.Core.Services.StateServicePatterns;
|
||||||
|
|
||||||
public class VirtualScroller
|
internal class VirtualScroller(MessageService messageService)
|
||||||
{
|
{
|
||||||
private Dictionary<string, VirtualScrollerState> States { get; } = new();
|
private Dictionary<string, VirtualScrollerState> States { get; } = new();
|
||||||
|
|
||||||
|
public VirtualScrollerState CreateStateObject()
|
||||||
|
{
|
||||||
|
return new VirtualScrollerState(messageService);
|
||||||
|
}
|
||||||
|
|
||||||
public void SetState(string id, VirtualScrollerState state)
|
public void SetState(string id, VirtualScrollerState state)
|
||||||
{
|
{
|
||||||
States[id] = state;
|
States[id] = state;
|
||||||
|
@ -18,11 +23,37 @@ public class VirtualScroller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VirtualScrollerState
|
internal class VirtualScrollerState : IDisposable
|
||||||
{
|
{
|
||||||
public Dictionary<string, int> Height = new();
|
internal VirtualScrollerState(MessageService messageService)
|
||||||
public int PadBottom = 0;
|
{
|
||||||
public int PadTop = 0;
|
_messageService = messageService;
|
||||||
public List<NoteResponse> RenderedList = [];
|
_messageService.AnyNoteChanged += OnNoteChanged;
|
||||||
public float ScrollTop = 0;
|
_messageService.AnyNoteDeleted += OnNoteDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, int> Height = new();
|
||||||
|
public int PadBottom = 0;
|
||||||
|
public int PadTop = 0;
|
||||||
|
public List<NoteResponse> RenderedList = [];
|
||||||
|
public float ScrollTop = 0;
|
||||||
|
private MessageService _messageService;
|
||||||
|
|
||||||
|
private void OnNoteChanged(object? _, NoteResponse note)
|
||||||
|
{
|
||||||
|
var i = RenderedList.FindIndex(p => p.Id == note.Id);
|
||||||
|
if (i >= 0) RenderedList[i] = note;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNoteDeleted(object? _, NoteResponse note)
|
||||||
|
{
|
||||||
|
var i = RenderedList.FindIndex(p => p.Id == note.Id);
|
||||||
|
if (i >= 0) RenderedList.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_messageService.AnyNoteChanged -= OnNoteChanged;
|
||||||
|
_messageService.AnyNoteDeleted -= OnNoteDeleted;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,15 +3,19 @@
|
||||||
@using Iceshrimp.Frontend.Components.Note
|
@using Iceshrimp.Frontend.Components.Note
|
||||||
@using Iceshrimp.Frontend.Core.Services
|
@using Iceshrimp.Frontend.Core.Services
|
||||||
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns
|
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns
|
||||||
|
@using Iceshrimp.Frontend.Localization
|
||||||
@using Iceshrimp.Shared.Schemas.Web
|
@using Iceshrimp.Shared.Schemas.Web
|
||||||
@inject ApiService ApiService
|
@using Microsoft.Extensions.Localization
|
||||||
@inject IJSRuntime Js
|
@inject ApiService ApiService
|
||||||
@inject MessageService MessageService
|
@inject IJSRuntime Js
|
||||||
@inject StateService State
|
@inject MessageService MessageService
|
||||||
@inject NavigationManager Navigation
|
@inject StateService State
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IStringLocalizer<Localization> Loc;
|
||||||
|
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@if (_init)
|
@if (_componentState == LoadState.Init)
|
||||||
{
|
{
|
||||||
<div @ref="Scroller" class="scroller">
|
<div @ref="Scroller" class="scroller">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
@ -41,14 +45,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
@if (_componentState == LoadState.Loading)
|
||||||
{
|
{
|
||||||
<div>Loading</div>
|
<div>Loading</div>
|
||||||
}
|
}
|
||||||
@if (_error)
|
@if (_componentState == LoadState.Error)
|
||||||
{
|
{
|
||||||
<div>This note does not exist!</div>
|
<div>This note does not exist!</div>
|
||||||
}
|
}
|
||||||
|
@if (_componentState == LoadState.Deleted)
|
||||||
|
{
|
||||||
|
<div>@Loc["This post has been deleted"]</div>
|
||||||
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public string? NoteId { get; set; }
|
[Parameter] public string? NoteId { get; set; }
|
||||||
|
@ -57,32 +65,39 @@ else
|
||||||
private IList<NoteResponse>? Ascendants { get; set; }
|
private IList<NoteResponse>? Ascendants { get; set; }
|
||||||
private IJSInProcessObjectReference Module { get; set; } = null!;
|
private IJSInProcessObjectReference Module { get; set; } = null!;
|
||||||
private ElementReference RootNoteRef { get; set; }
|
private ElementReference RootNoteRef { get; set; }
|
||||||
private bool _init;
|
|
||||||
private bool _error;
|
|
||||||
private int _depth = 20;
|
private int _depth = 20;
|
||||||
private ElementReference Scroller { get; set; }
|
private ElementReference Scroller { get; set; }
|
||||||
private IDisposable? _locationChangingHandlerDisposable;
|
private IDisposable? _locationChangingHandlerDisposable;
|
||||||
private IDisposable? _noteChangedHandler;
|
private IDisposable? _noteChangedHandler;
|
||||||
|
private LoadState _componentState;
|
||||||
|
|
||||||
|
private enum LoadState
|
||||||
|
{
|
||||||
|
Loading,
|
||||||
|
Init,
|
||||||
|
Error,
|
||||||
|
Deleted
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
_init = false;
|
_componentState = LoadState.Loading;
|
||||||
if (NoteId == null)
|
if (NoteId == null)
|
||||||
{
|
{
|
||||||
_error = true;
|
_componentState = LoadState.Error;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RootNote = await ApiService.Notes.GetNote(NoteId);
|
RootNote = await ApiService.Notes.GetNote(NoteId);
|
||||||
if (RootNote == null)
|
if (RootNote == null)
|
||||||
{
|
{
|
||||||
_error = true;
|
_componentState = LoadState.Error;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Descendants = await ApiService.Notes.GetNoteDescendants(NoteId, _depth);
|
Descendants = await ApiService.Notes.GetNoteDescendants(NoteId, _depth);
|
||||||
Ascendants = await ApiService.Notes.GetNoteAscendants(NoteId, default);
|
Ascendants = await ApiService.Notes.GetNoteAscendants(NoteId, default);
|
||||||
_init = true;
|
_componentState = LoadState.Init;
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
@ -90,7 +105,8 @@ else
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
if (NoteId != null) _noteChangedHandler = MessageService.Register(NoteId, OnNoteChanged, MessageService.Type.Updated);
|
if (NoteId != null) _noteChangedHandler = MessageService.Register(NoteId, OnNoteChanged, MessageService.Type.Updated);
|
||||||
_locationChangingHandlerDisposable = Navigation.RegisterLocationChangingHandler(LocationChangeHandler);
|
_locationChangingHandlerDisposable = Navigation.RegisterLocationChangingHandler(LocationChangeHandler);
|
||||||
|
MessageService.AnyNoteDeleted += OnNoteDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNoteChanged(object? _, NoteResponse note)
|
private void OnNoteChanged(object? _, NoteResponse note)
|
||||||
|
@ -98,6 +114,21 @@ else
|
||||||
var __ = Refresh();
|
var __ = Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnNoteDeleted(object? _, NoteResponse note)
|
||||||
|
{
|
||||||
|
if (NoteId == note.Id)
|
||||||
|
{
|
||||||
|
_componentState = LoadState.Deleted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Ascendants?.Remove(note);
|
||||||
|
Descendants?.Remove(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Refresh()
|
private async Task Refresh()
|
||||||
{
|
{
|
||||||
if (NoteId == null) throw new InvalidOperationException("RefreshNote called under impossible circumstances");
|
if (NoteId == null) throw new InvalidOperationException("RefreshNote called under impossible circumstances");
|
||||||
|
@ -119,7 +150,7 @@ else
|
||||||
Module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "/Pages/SingleNote.razor.js");
|
Module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "/Pages/SingleNote.razor.js");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_init)
|
if (_componentState == LoadState.Init)
|
||||||
{
|
{
|
||||||
var state = State.SingleNote.GetState(NoteId!);
|
var state = State.SingleNote.GetState(NoteId!);
|
||||||
if (state != null)
|
if (state != null)
|
||||||
|
@ -135,7 +166,7 @@ else
|
||||||
|
|
||||||
private void SaveState()
|
private void SaveState()
|
||||||
{
|
{
|
||||||
if (NoteId == null) return;
|
if (NoteId == null || _componentState != LoadState.Init) return;
|
||||||
var scrollTop = Module.Invoke<float>("GetScrollTop", Scroller);
|
var scrollTop = Module.Invoke<float>("GetScrollTop", Scroller);
|
||||||
var state = new SingleNoteState { ScrollTop = scrollTop };
|
var state = new SingleNoteState { ScrollTop = scrollTop };
|
||||||
State.SingleNote.SetState(NoteId, state);
|
State.SingleNote.SetState(NoteId, state);
|
||||||
|
@ -146,5 +177,6 @@ else
|
||||||
if (_noteChangedHandler != null) _noteChangedHandler.Dispose();
|
if (_noteChangedHandler != null) _noteChangedHandler.Dispose();
|
||||||
SaveState();
|
SaveState();
|
||||||
_locationChangingHandlerDisposable?.Dispose();
|
_locationChangingHandlerDisposable?.Dispose();
|
||||||
|
MessageService.AnyNoteDeleted -= OnNoteDeleted;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue