From 56325b356c96694c3e76e15769030491d2e31615 Mon Sep 17 00:00:00 2001 From: Lilian Date: Sun, 19 Jan 2025 20:52:06 +0100 Subject: [PATCH] [frontend] Implement timeline streaming --- .../Components/VirtualScroller.cs | 18 ++++++++- .../Miscellaneous/IStreamingItemProvider.cs | 8 ++++ .../Services/NoteStore/StateSynchronizer.cs | 20 +++++++++- .../Core/Services/NoteStore/TimelineStore.cs | 40 ++++++++++++++++--- Iceshrimp.Frontend/Pages/TimelinePage.razor | 6 +-- 5 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 Iceshrimp.Frontend/Core/Miscellaneous/IStreamingItemProvider.cs diff --git a/Iceshrimp.Frontend/Components/VirtualScroller.cs b/Iceshrimp.Frontend/Components/VirtualScroller.cs index 9d26331e..8894158e 100644 --- a/Iceshrimp.Frontend/Components/VirtualScroller.cs +++ b/Iceshrimp.Frontend/Components/VirtualScroller.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Frontend.Core.Miscellaneous; using Iceshrimp.Frontend.Core.Services; using Iceshrimp.Frontend.Core.Services.StateServicePatterns; using Iceshrimp.Frontend.Enums; @@ -24,6 +25,7 @@ public class VirtualScroller : ComponentBase, IDisposable where T : IIdentifi [Parameter] [EditorRequired] public required Func?>> ItemProvider { get; set; } [Parameter] [EditorRequired] public required string StateKey { get; set; } [Parameter] [EditorRequired] public required Func, List> ItemProviderById { get; set; } + [Parameter] public IStreamingItemProvider? StreamingItemProvider { get; set; } private ScrollEnd Before { get; set; } = null!; private ScrollEnd After { get; set; } = null!; private Dictionary Children { get; set; } = new(); @@ -80,6 +82,8 @@ public class VirtualScroller : ComponentBase, IDisposable where T : IIdentifi _setScroll = true; } + if (StreamingItemProvider != null) StreamingItemProvider.ItemPublished += OnNewItem; + ReRender(); _initialized = true; } @@ -183,6 +187,17 @@ public class VirtualScroller : ComponentBase, IDisposable where T : IIdentifi 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() { if (!_initialized) @@ -216,7 +231,7 @@ public class VirtualScroller : ComponentBase, IDisposable where T : IIdentifi { _module.InvokeVoid("SetScrollY", scrollY); } - + private void Save() { var scrollY = GetScrollY(); @@ -239,5 +254,6 @@ public class VirtualScroller : ComponentBase, IDisposable where T : IIdentifi public void Dispose() { _locationChangeHandlerDisposable?.Dispose(); + if (StreamingItemProvider != null) StreamingItemProvider.ItemPublished -= OnNewItem; } } diff --git a/Iceshrimp.Frontend/Core/Miscellaneous/IStreamingItemProvider.cs b/Iceshrimp.Frontend/Core/Miscellaneous/IStreamingItemProvider.cs new file mode 100644 index 00000000..0b1c4624 --- /dev/null +++ b/Iceshrimp.Frontend/Core/Miscellaneous/IStreamingItemProvider.cs @@ -0,0 +1,8 @@ +using Iceshrimp.Shared.Helpers; + +namespace Iceshrimp.Frontend.Core.Miscellaneous; + +public interface IStreamingItemProvider where T : IIdentifiable +{ + event EventHandler ItemPublished; +} diff --git a/Iceshrimp.Frontend/Core/Services/NoteStore/StateSynchronizer.cs b/Iceshrimp.Frontend/Core/Services/NoteStore/StateSynchronizer.cs index a180c4ba..53d51306 100644 --- a/Iceshrimp.Frontend/Core/Services/NoteStore/StateSynchronizer.cs +++ b/Iceshrimp.Frontend/Core/Services/NoteStore/StateSynchronizer.cs @@ -2,10 +2,17 @@ using Iceshrimp.Shared.Schemas.Web; namespace Iceshrimp.Frontend.Core.Services.NoteStore; -internal class StateSynchronizer +internal class StateSynchronizer: IAsyncDisposable { + private readonly StreamingService _streamingService; public event EventHandler? NoteChanged; public event EventHandler? NoteDeleted; + + public StateSynchronizer(StreamingService streamingService) + { + _streamingService = streamingService; + _streamingService.NoteUpdated += NoteUpdated; + } public void Broadcast(NoteBase note) { @@ -16,4 +23,15 @@ internal class StateSynchronizer { 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(); + } } diff --git a/Iceshrimp.Frontend/Core/Services/NoteStore/TimelineStore.cs b/Iceshrimp.Frontend/Core/Services/NoteStore/TimelineStore.cs index 8a83054e..36f164f1 100644 --- a/Iceshrimp.Frontend/Core/Services/NoteStore/TimelineStore.cs +++ b/Iceshrimp.Frontend/Core/Services/NoteStore/TimelineStore.cs @@ -1,22 +1,28 @@ using Iceshrimp.Frontend.Core.Miscellaneous; using Iceshrimp.Frontend.Enums; +using Iceshrimp.Shared.Schemas.SignalR; 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; -internal class TimelineStore : NoteMessageProvider, IDisposable +internal class TimelineStore : NoteMessageProvider, IAsyncDisposable, IStreamingItemProvider { + public event EventHandler? ItemPublished; private Dictionary Timelines { get; set; } = new(); private readonly ApiService _api; private readonly ILogger _logger; private readonly StateSynchronizer _stateSynchronizer; + private readonly StreamingService _streamingService; - public TimelineStore(ApiService api, ILogger logger, StateSynchronizer stateSynchronizer) + public TimelineStore(ApiService api, ILogger logger, StateSynchronizer stateSynchronizer, StreamingService streamingService) { - _api = api; - _logger = logger; - _stateSynchronizer = stateSynchronizer; - _stateSynchronizer.NoteChanged += OnNoteChanged; + _api = api; + _logger = logger; + _stateSynchronizer = stateSynchronizer; + _streamingService = streamingService; + _stateSynchronizer.NoteChanged += OnNoteChanged; + _streamingService.NotePublished += OnNotePublished; } private void OnNoteChanged(object? _, NoteBase changedNote) @@ -152,6 +158,21 @@ internal class TimelineStore : NoteMessageProvider, IDisposable 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 required DirectionEnum Direction { get; set; } @@ -163,4 +184,11 @@ internal class TimelineStore : NoteMessageProvider, IDisposable { _stateSynchronizer.NoteChanged -= OnNoteChanged; } + + public async ValueTask DisposeAsync() + { + await _stateSynchronizer.DisposeAsync(); + await _streamingService.DisposeAsync(); + } + } diff --git a/Iceshrimp.Frontend/Pages/TimelinePage.razor b/Iceshrimp.Frontend/Pages/TimelinePage.razor index 101c17d8..1a6e17b6 100644 --- a/Iceshrimp.Frontend/Pages/TimelinePage.razor +++ b/Iceshrimp.Frontend/Pages/TimelinePage.razor @@ -21,7 +21,7 @@ @if (NoteResponses is not null) { - + @@ -33,10 +33,10 @@ @code { - private List? NoteResponses { get; set; } + private List? NoteResponses { get; set; } 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(); }