@implements IAsyncDisposable @* The app-wide state, cascaded to every page and component (LocalizableComponentBase.CascadingState): the saved PublicCacheData with its theme settings, the signed-in user, and whether the app is online, polled every 10 seconds. *@ @ChildContent @code { [Parameter] public RenderFragment ChildContent { get; set; } [Inject] ILocalStorageService Storage { get; set; } [Inject] IJSRuntime JS { get; set; } [Inject] public AppStatusService Status { get; set; } [Inject] ILoggingService Logger { get; set; } [Inject] ToastService Toast { get; set; } public bool IsOnline { get; private set; } = true; public bool IsOffline => !IsOnline; public PublicCacheData PublicCacheData { get; private set; } = new(); /// The signed-in user; Faker's until the login is wired to PrivaPub. public User User { get; private set; } = Faker.CurrentUser; DotNetObjectReference cascadingStateReference; PeriodicTimer onlineTimer; readonly CancellationTokenSource disposeCancellation = new(); protected override async Task OnInitializedAsync() { var cancellationToken = disposeCancellation.Token; try { cascadingStateReference = DotNetObjectReference.Create(this); await JS.InvokeVoidAsync("cascadingStateInstanceReference", cancellationToken, cascadingStateReference); var publicCacheData = await Storage.GetItemAsync(nameof(PublicCacheData), cancellationToken); await UpdatePublicCache(publicCacheData ?? new(), cancellationToken); IsOnline = await Status.IsOnline(cancellationToken); onlineTimer = new(TimeSpan.FromSeconds(10)); _ = PollOnline(cancellationToken); } catch (Exception ex) { await Logger.ProcessError(ex, $"{nameof(CascadingState)}.{nameof(OnInitializedAsync)}", cancellationToken); } } /// Saves the public cache, applies its theme and re-renders the app. public async ValueTask UpdatePublicCache(PublicCacheData publicCacheData, CancellationToken cancellationToken = default) { PublicCacheData = publicCacheData; await Storage.SetItemAsync(nameof(PublicCacheData), PublicCacheData, cancellationToken); Status.ApplyTheme(PublicCacheData.PageSettings); StateHasChanged(); } /// Changes the page settings, then saves and applies them like UpdatePublicCache. public ValueTask UpdatePageSettings(Action change, CancellationToken cancellationToken = default) { change(PublicCacheData.PageSettings); return UpdatePublicCache(PublicCacheData, cancellationToken); } [JSInvokable] public async Task LogFromJs(string message, string where) => await ProcessError(new(message), where); async Task PollOnline(CancellationToken cancellationToken) { try { while (await onlineTimer.WaitForNextTickAsync(cancellationToken)) { var isOnline = await Status.IsOnline(cancellationToken); if (isOnline == IsOnline) continue; IsOnline = isOnline; await InvokeAsync(StateHasChanged); } } catch (OperationCanceledException) { } catch (Exception ex) { await Logger.ProcessError(ex, $"{nameof(CascadingState)}.{nameof(PollOnline)}", cancellationToken); } } /// Logs an unexpected failure and tells the user. public async ValueTask ProcessError(Exception exception, string where, CancellationToken cancellationToken = default) { await Logger.ProcessError(exception, where, cancellationToken); Toast.AddException(exception); } public async ValueTask ProcessWarning(string message, string where, CancellationToken cancellationToken = default) { await Logger.ProcessWarning(message, where, cancellationToken); Toast.AddWarning(message); } public async ValueTask DisposeAsync() { await disposeCancellation.CancelAsync(); disposeCancellation.Dispose(); onlineTimer?.Dispose(); cascadingStateReference?.Dispose(); } }