From 95428430c007341a5777b7760fc1d29add54b7d7 Mon Sep 17 00:00:00 2001 From: thepra Date: Sat, 3 Oct 2026 15:26:21 +0200 Subject: [PATCH] Adopt b2b.next's client patterns: services, page state, feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from the owner's b2b.next and adapted from MudBlazor to the daisyUI components: - LocalizableComponentBase provides CascadingState, Navigation, Toast, IsLoading/IsDefaultDisabled, WhenOnline/StartOnlinePolling, and after-render jobs that actually run. - CascadingState starts with initialised state and polls the online status with a PeriodicTimer. It reports errors through ILoggingService plus a toast, and disposes what it holds. - New pieces: - ToastService + ToastHost, the snackbar, with AddResponseError/Success/ Warning/Exception in SUtility; - ErrorViewer, which shows the validation messages plus AdditionalError(s); - LoggingService, which writes to the console and the IndexedDB client log. - HttpService is rebuilt and registered: - auth levels Get/Post/Delete, *Anon and *TotallyAnon; - retry with backoff; - classified failures (FailureCodes) instead of exceptions; - HTTP 410 on cancel; - the "default" client. ReadWebResult and DefaultReadFromJsonAsync read its responses. - ClientJsonContext is source-generated, and SUtility.DefaultSerializer and Blazored.LocalStorage use it. - WebResult gains a C# 14 IsInvalid extension property. The code follows the imported rules: - Every service and async helper takes a CancellationToken last. - Primary constructors replace constructor-plus-fields. - MessagesService and IStorage return Task / Task>. MessagesService is still mock, building its results in memory; the seed feed and thread moved to Faker. - Index and ExpandMessage follow the page pattern: an offline guard with WhenOnline, LoadData, and early returns with toasts on the results they used to ignore. ExpandMessage reloads on route change and shows "not found" instead of a null message. - Model and state collections are [], and the fake identity defaults moved out of the models. - Components: - no ?. chains over initialised state; - [Inject] instead of @inject; - IsDefaultDisabled on the actions; - autocomplete on every text input; - Localizer[key, args] instead of string.Format; - the localizer no longer leaks its inner _pLocalizer. - Settings → General gains a language picker (English/Italiano), and SetDefaultCulture limits the culture to those two. Bugs fixed along the way: - A compose with untouched (null) content passed the empty check. - File errors were joined with
and rendered as markup. - ClientLogs.ErrorMessage() threw on warnings. - CoalescingStringLocalizer[key, args] showed a raw "{0}" for missing keys. - AppStatusService exposed a call to a JS function that doesn't exist. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- App.razor | 12 +- ClientJsonContext.cs | 36 ++ Components/ActionBar.razor | 15 +- Components/Content.razor | 42 +- Components/Daisy/DataDisplay/DAccordion.razor | 4 +- Components/Daisy/DataDisplay/DAvatar.razor | 1 + Components/Daisy/DataDisplay/DCarousel.razor | 6 +- .../Daisy/DataDisplay/DHoverGallery.razor | 1 + Components/Daisy/Navigation/DTabs.razor | 4 +- Components/ErrorViewer.razor | 58 +++ Components/ErrorViewerSeverity.cs | 7 + Components/MessageUpsertForm.razor | 236 ++++----- Components/Styleguide/DataInputSection.razor | 56 +-- Components/Styleguide/FeedbackSection.razor | 2 +- Components/Styleguide/LayoutSection.razor | 2 +- Components/ToastHost.razor | 41 ++ Extensions/ExtensionMethods.cs | 113 +++-- Extensions/GenericExtensions.cs | 33 +- Extensions/WebResultExtensions.cs | 12 + Helpers/Faker.cs | 258 +++++++++- Helpers/SUtility.cs | 98 +++- LayerComponents/CascadingState.razor | 151 +++--- LayerComponents/LocalizableComponentBase.cs | 103 +++- Models/BoostingUser.cs | 10 +- Models/ClientLogs.cs | 20 +- Models/FailureCodes.cs | 20 + Models/Media.cs | 2 +- Models/Message.cs | 2 +- Models/MessageForm.cs | 2 +- Models/MessageUser.cs | 12 +- Models/PublicCacheData.cs | 6 +- Models/UploadMedia.cs | 2 +- Models/User.cs | 33 +- Models/ValidationErrors.cs | 11 + Pages/Administration.razor | 1 - Pages/ExpandMessage.razor | 219 +++++---- Pages/Index.razor | 240 ++++------ Pages/Login.razor | 1 - Pages/Logout.razor | 2 - Pages/Settings.razor | 31 +- Program.cs | 8 +- Services/AppStatusService.cs | 34 +- Services/CoalescingStringLocalizer.cs | 75 +-- Services/IHttpService.cs | 343 ++++++------- Services/ILoggingService.cs | 61 +++ Services/IStorage.cs | 452 +++++------------- Services/IndexedDb.cs | 8 +- Services/MessagesService.cs | 144 +++--- Services/ToastService.cs | 55 +++ Services/TokenAuthStateProvider.cs | 53 +- Shared/LoginDisplay.razor | 2 - Shared/MainLayout.razor | 4 +- Shared/NavMenu.razor | 22 +- Shared/RedirectToLogin.razor | 14 +- _Imports.razor | 1 + 55 files changed, 1844 insertions(+), 1337 deletions(-) create mode 100644 ClientJsonContext.cs create mode 100644 Components/ErrorViewer.razor create mode 100644 Components/ErrorViewerSeverity.cs create mode 100644 Components/ToastHost.razor create mode 100644 Extensions/WebResultExtensions.cs create mode 100644 Models/FailureCodes.cs create mode 100644 Models/ValidationErrors.cs create mode 100644 Services/ILoggingService.cs create mode 100644 Services/ToastService.cs diff --git a/App.razor b/App.razor index 56aa443..abb2f19 100644 --- a/App.razor +++ b/App.razor @@ -10,18 +10,22 @@ } else { -

You are not authorized to access this resource.

+

@Localizer["You are not authorized to access this resource."]

} - Not found + @Localizer["Not found"] -

Sorry, there's nothing at this address.

+

@Localizer["Sorry, there's nothing at this address."]

- \ No newline at end of file + + +@code { + [Inject] CoalescingStringLocalizer Localizer { get; set; } +} diff --git a/ClientJsonContext.cs b/ClientJsonContext.cs new file mode 100644 index 0000000..3b27a6a --- /dev/null +++ b/ClientJsonContext.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +using decePubClient.Models; + +using PrivaPub.ClientModels; + +namespace decePubClient; + +/// +/// Source-generated JSON metadata for the client's own types, used first by SUtility.DefaultSerializer (HTTP) and by +/// Blazored.LocalStorage (Program.cs). Register every type that crosses HTTP or is stored in localStorage here; types +/// missing from it still work through the reflection fallback. Names stay camelCase: wwwroot/js/theme.js reads +/// PublicCacheData.pageSettings straight from localStorage. +/// +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyProperties = true)] +[JsonSerializable(typeof(PublicCacheData))] +[JsonSerializable(typeof(PageSettings))] +[JsonSerializable(typeof(AuthData))] +[JsonSerializable(typeof(User))] +[JsonSerializable(typeof(UserClaim))] +[JsonSerializable(typeof(ActionBarFilter))] +[JsonSerializable(typeof(Message))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(MessageUser))] +[JsonSerializable(typeof(BoostingUser))] +[JsonSerializable(typeof(Media))] +[JsonSerializable(typeof(MessageForm))] +[JsonSerializable(typeof(UploadMedia))] +[JsonSerializable(typeof(ClientLogs))] +[JsonSerializable(typeof(ClientLogException))] +[JsonSerializable(typeof(ValidationErrors))] +[JsonSerializable(typeof(WebResult))] +public partial class ClientJsonContext : JsonSerializerContext +{ +} diff --git a/Components/ActionBar.razor b/Components/ActionBar.razor index 30f823d..ccde121 100644 --- a/Components/ActionBar.razor +++ b/Components/ActionBar.razor @@ -65,33 +65,38 @@ @code { - [CascadingParameter] CascadingState CascadingState { get; set; } [Parameter] public EventCallback OnMessageSubmit { get; set; } [Parameter] public EventCallback OnTimelineChanged { get; set; } [Parameter] public EventCallback OnTimeSortingChanged { get; set; } [Inject] ILocalStorageService LocalStorage { get; set; } - bool isPosting { get; set; } = false; + bool isPosting; ActionBarFilter Filters { get; set; } = new(); protected override async Task OnInitializedAsync() { var filters = await LocalStorage.GetItemAsync(nameof(ActionBarFilter)); - if (filters == default) - await LocalStorage.SetItemAsync(nameof(ActionBarFilter), Filters); - else + IsLoading = false; + if (filters is not null) + { Filters = filters; + return; + } + + await LocalStorage.SetItemAsync(nameof(ActionBarFilter), Filters); } void OpenCloseMessageForm() { isPosting = !isPosting; } + async Task OnTimelineChange(TimelineType timelineType) { Filters.TimelineType = timelineType; await LocalStorage.SetItemAsync(nameof(ActionBarFilter), Filters); await OnTimelineChanged.InvokeAsync(Filters.TimelineType); } + async Task OnTimeSortChange(TimeSortingType timeSorting) { Filters.TimeSortingType = timeSorting; diff --git a/Components/Content.razor b/Components/Content.razor index e4b51fa..c0fd16c 100644 --- a/Components/Content.razor +++ b/Components/Content.razor @@ -3,7 +3,7 @@
+ style="background-image:linear-gradient(to left, var(--color-base-100), transparent), url('@(Message.User.BackgroundUrl)');"> @Message.User.UserName @@ -32,8 +32,8 @@ - - @Message.CreatedAt.GetPassedTime(Localizer._pLocalizer) + + @Message.CreatedAt.GetPassedTime(Localizer)