[frontend] Add note deletion to VirtualScroller and Timeline (ISH-404)

This commit is contained in:
Lilian 2024-07-21 22:27:54 +02:00
parent 854979b359
commit ba42e19beb
No known key found for this signature in database
5 changed files with 114 additions and 38 deletions

View file

@ -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;
} }
} }
} }

View file

@ -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();
} }

View file

@ -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()

View file

@ -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;
}
} }

View file

@ -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;
} }
} }