[frontend] Implement timeline streaming

This commit is contained in:
Lilian 2025-01-19 20:52:06 +01:00
parent 213801ed4b
commit 56325b356c
No known key found for this signature in database
5 changed files with 81 additions and 11 deletions

View file

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

View file

@ -0,0 +1,8 @@
using Iceshrimp.Shared.Helpers;
namespace Iceshrimp.Frontend.Core.Miscellaneous;
public interface IStreamingItemProvider<T> where T : IIdentifiable
{
event EventHandler<T> ItemPublished;
}

View file

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

View file

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

View file

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