[frontend] Implement timeline streaming
This commit is contained in:
parent
213801ed4b
commit
56325b356c
5 changed files with 81 additions and 11 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
using Iceshrimp.Frontend.Core.Miscellaneous;
|
||||||
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.Enums;
|
using Iceshrimp.Frontend.Enums;
|
||||||
|
@ -24,6 +25,7 @@ public class VirtualScroller<T> : ComponentBase, IDisposable where T : IIdentifi
|
||||||
[Parameter] [EditorRequired] public required Func<DirectionEnum, T, Task<List<T>?>> ItemProvider { get; set; }
|
[Parameter] [EditorRequired] public required Func<DirectionEnum, T, Task<List<T>?>> ItemProvider { get; set; }
|
||||||
[Parameter] [EditorRequired] public required string StateKey { get; set; }
|
[Parameter] [EditorRequired] public required string StateKey { get; set; }
|
||||||
[Parameter] [EditorRequired] public required Func<List<string>, List<T>> ItemProviderById { get; set; }
|
[Parameter] [EditorRequired] public required Func<List<string>, List<T>> ItemProviderById { get; set; }
|
||||||
|
[Parameter] public IStreamingItemProvider<T>? StreamingItemProvider { get; set; }
|
||||||
private ScrollEnd Before { get; set; } = null!;
|
private ScrollEnd Before { get; set; } = null!;
|
||||||
private ScrollEnd After { get; set; } = null!;
|
private ScrollEnd After { get; set; } = null!;
|
||||||
private Dictionary<string, LazyComponent> Children { get; set; } = new();
|
private Dictionary<string, LazyComponent> Children { get; set; } = new();
|
||||||
|
@ -80,6 +82,8 @@ public class VirtualScroller<T> : ComponentBase, IDisposable where T : IIdentifi
|
||||||
_setScroll = true;
|
_setScroll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StreamingItemProvider != null) StreamingItemProvider.ItemPublished += OnNewItem;
|
||||||
|
|
||||||
ReRender();
|
ReRender();
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
@ -183,6 +187,17 @@ public class VirtualScroller<T> : ComponentBase, IDisposable where T : IIdentifi
|
||||||
Before.Reset();
|
Before.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnNewItem(object? _, T item)
|
||||||
|
{
|
||||||
|
var height = GetScrollY();
|
||||||
|
if (height == 0)
|
||||||
|
{
|
||||||
|
var add = Items.TryAdd(item.Id, item);
|
||||||
|
if (add is false) Logger.LogError($"Duplicate notification: {item.Id}");
|
||||||
|
}
|
||||||
|
ReRender();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CallbackAfterAsync()
|
private async Task CallbackAfterAsync()
|
||||||
{
|
{
|
||||||
if (!_initialized)
|
if (!_initialized)
|
||||||
|
@ -239,5 +254,6 @@ public class VirtualScroller<T> : ComponentBase, IDisposable where T : IIdentifi
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_locationChangeHandlerDisposable?.Dispose();
|
_locationChangeHandlerDisposable?.Dispose();
|
||||||
|
if (StreamingItemProvider != null) StreamingItemProvider.ItemPublished -= OnNewItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
using Iceshrimp.Shared.Helpers;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Frontend.Core.Miscellaneous;
|
||||||
|
|
||||||
|
public interface IStreamingItemProvider<T> where T : IIdentifiable
|
||||||
|
{
|
||||||
|
event EventHandler<T> ItemPublished;
|
||||||
|
}
|
|
@ -2,11 +2,18 @@ using Iceshrimp.Shared.Schemas.Web;
|
||||||
|
|
||||||
namespace Iceshrimp.Frontend.Core.Services.NoteStore;
|
namespace Iceshrimp.Frontend.Core.Services.NoteStore;
|
||||||
|
|
||||||
internal class StateSynchronizer
|
internal class StateSynchronizer: IAsyncDisposable
|
||||||
{
|
{
|
||||||
|
private readonly StreamingService _streamingService;
|
||||||
public event EventHandler<NoteBase>? NoteChanged;
|
public event EventHandler<NoteBase>? NoteChanged;
|
||||||
public event EventHandler<NoteBase>? NoteDeleted;
|
public event EventHandler<NoteBase>? NoteDeleted;
|
||||||
|
|
||||||
|
public StateSynchronizer(StreamingService streamingService)
|
||||||
|
{
|
||||||
|
_streamingService = streamingService;
|
||||||
|
_streamingService.NoteUpdated += NoteUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
public void Broadcast(NoteBase note)
|
public void Broadcast(NoteBase note)
|
||||||
{
|
{
|
||||||
NoteChanged?.Invoke(this, note);
|
NoteChanged?.Invoke(this, note);
|
||||||
|
@ -16,4 +23,15 @@ internal class StateSynchronizer
|
||||||
{
|
{
|
||||||
NoteDeleted?.Invoke(this, note);
|
NoteDeleted?.Invoke(this, note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void NoteUpdated(object? sender, NoteResponse noteResponse)
|
||||||
|
{
|
||||||
|
NoteChanged?.Invoke(this, noteResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_streamingService.NoteUpdated -= NoteUpdated;
|
||||||
|
await _streamingService.DisposeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
using Iceshrimp.Frontend.Core.Miscellaneous;
|
using Iceshrimp.Frontend.Core.Miscellaneous;
|
||||||
using Iceshrimp.Frontend.Enums;
|
using Iceshrimp.Frontend.Enums;
|
||||||
|
using Iceshrimp.Shared.Schemas.SignalR;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
|
using NoteEvent = (Iceshrimp.Shared.Schemas.SignalR.StreamingTimeline timeline, Iceshrimp.Shared.Schemas.Web.NoteResponse note);
|
||||||
|
|
||||||
namespace Iceshrimp.Frontend.Core.Services.NoteStore;
|
namespace Iceshrimp.Frontend.Core.Services.NoteStore;
|
||||||
|
|
||||||
internal class TimelineStore : NoteMessageProvider, IDisposable
|
internal class TimelineStore : NoteMessageProvider, IAsyncDisposable, IStreamingItemProvider<NoteResponse>
|
||||||
{
|
{
|
||||||
|
public event EventHandler<NoteResponse>? ItemPublished;
|
||||||
private Dictionary<string, TimelineState> Timelines { get; set; } = new();
|
private Dictionary<string, TimelineState> Timelines { get; set; } = new();
|
||||||
private readonly ApiService _api;
|
private readonly ApiService _api;
|
||||||
private readonly ILogger<TimelineStore> _logger;
|
private readonly ILogger<TimelineStore> _logger;
|
||||||
private readonly StateSynchronizer _stateSynchronizer;
|
private readonly StateSynchronizer _stateSynchronizer;
|
||||||
|
private readonly StreamingService _streamingService;
|
||||||
|
|
||||||
public TimelineStore(ApiService api, ILogger<TimelineStore> logger, StateSynchronizer stateSynchronizer)
|
public TimelineStore(ApiService api, ILogger<TimelineStore> logger, StateSynchronizer stateSynchronizer, StreamingService streamingService)
|
||||||
{
|
{
|
||||||
_api = api;
|
_api = api;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_stateSynchronizer = stateSynchronizer;
|
_stateSynchronizer = stateSynchronizer;
|
||||||
_stateSynchronizer.NoteChanged += OnNoteChanged;
|
_streamingService = streamingService;
|
||||||
|
_stateSynchronizer.NoteChanged += OnNoteChanged;
|
||||||
|
_streamingService.NotePublished += OnNotePublished;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNoteChanged(object? _, NoteBase changedNote)
|
private void OnNoteChanged(object? _, NoteBase changedNote)
|
||||||
|
@ -152,6 +158,21 @@ internal class TimelineStore : NoteMessageProvider, IDisposable
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnNotePublished(object? sender, NoteEvent valueTuple)
|
||||||
|
{
|
||||||
|
var (timeline, response) = valueTuple;
|
||||||
|
if (timeline == StreamingTimeline.Home)
|
||||||
|
{
|
||||||
|
var success = Timelines.TryGetValue("home", out var home);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var add = home!.Timeline.TryAdd(response.Id, response);
|
||||||
|
if (add is false) _logger.LogError($"Duplicate note: {response.Id}");
|
||||||
|
}
|
||||||
|
ItemPublished?.Invoke(this, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class Cursor
|
public class Cursor
|
||||||
{
|
{
|
||||||
public required DirectionEnum Direction { get; set; }
|
public required DirectionEnum Direction { get; set; }
|
||||||
|
@ -163,4 +184,11 @@ internal class TimelineStore : NoteMessageProvider, IDisposable
|
||||||
{
|
{
|
||||||
_stateSynchronizer.NoteChanged -= OnNoteChanged;
|
_stateSynchronizer.NoteChanged -= OnNoteChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await _stateSynchronizer.DisposeAsync();
|
||||||
|
await _streamingService.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
@if (NoteResponses is not null)
|
@if (NoteResponses is not null)
|
||||||
{
|
{
|
||||||
|
|
||||||
<VirtualScroller InitialItems="NoteResponses" ItemProvider="Provider" StateKey="1234567890" ItemProviderById="ItemProviderById">
|
<VirtualScroller InitialItems="NoteResponses" ItemProvider="Provider" StateKey="1234567890" ItemProviderById="ItemProviderById" StreamingItemProvider="Store">
|
||||||
<ItemTemplate Context="note">
|
<ItemTemplate Context="note">
|
||||||
<CascadingValue Value="Store" TValue="NoteMessageProvider" Name="Provider">
|
<CascadingValue Value="Store" TValue="NoteMessageProvider" Name="Provider">
|
||||||
<TimelineNote Note="note"></TimelineNote>
|
<TimelineNote Note="note"></TimelineNote>
|
||||||
|
@ -33,10 +33,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<NoteResponse>? NoteResponses { get; set; }
|
private List<NoteResponse>? NoteResponses { get; set; }
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
NoteResponses = await Store.GetHomeTimelineAsync("home", new TimelineStore.Cursor { Direction = DirectionEnum.Older, Count = 20, Id = null });
|
NoteResponses = await Store.GetHomeTimelineAsync("home", new TimelineStore.Cursor { Direction = DirectionEnum.Older, Count = 20, Id = null });
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue