From c73c41ca060f6dd78d27142d8ae20f80d52ceec0 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 26 Nov 2024 18:33:35 +0100 Subject: [PATCH] Refactor RecentEmails component to only load emails when app is visible (#426) --- .../Main/Components/Email/RecentEmails.razor | 175 +++++++++++++----- .../Services/JsInteropService.cs | 11 ++ src/AliasVault.Client/wwwroot/js/utilities.js | 6 + 3 files changed, 149 insertions(+), 43 deletions(-) diff --git a/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor b/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor index 6ff06473f..781affe3f 100644 --- a/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor +++ b/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor @@ -26,7 +26,7 @@

Email

- @if (RefreshTimer is not null) + @if (DbService.Settings.AutoEmailRefresh) {
} @@ -56,27 +56,27 @@
- - - - + + + + - @foreach (var mail in MailboxEmails) - { - - - - - } + @foreach (var mail in MailboxEmails) + { + + + + + }
- Subject - - Date & Time -
+ Subject + + Date & Time +
- @(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))... - - @mail.DateSystem -
+ @(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))... + + @mail.DateSystem +
@@ -99,13 +99,53 @@ private EmailApiModel Email { get; set; } = new(); private bool EmailModalVisible { get; set; } private string Error { get; set; } = string.Empty; - private Timer? RefreshTimer { get; set; } private bool IsRefreshing { get; set; } = true; private bool IsLoading { get; set; } = true; private bool IsSpamOk { get; set; } = false; + private bool IsPageVisible { get; set; } = true; + private CancellationTokenSource? PollingCancellationTokenSource { get; set; } + private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds + private readonly SemaphoreSlim RefreshSemaphore = new(1, 1); + private DateTime LastRefreshTime = DateTime.MinValue; + + /// + /// Callback invoked by JavaScript when the page visibility changes. + /// + /// Boolean whether the page is visible or not. + /// Task. + [JSInvokable] + public async Task OnVisibilityChange(bool isVisible) + { + IsPageVisible = isVisible; + if (isVisible) + { + // Only enable auto-refresh if the setting is enabled. + if (DbService.Settings.AutoEmailRefresh) + { + await StartPolling(); + } + + // Refresh immediately when tab becomes visible + await ManualRefresh(); + } + else + { + // Cancel polling. + PollingCancellationTokenSource?.Cancel(); + } + StateHasChanged(); + } + + /// + public void Dispose() + { + PollingCancellationTokenSource?.Cancel(); + PollingCancellationTokenSource?.Dispose(); + RefreshSemaphore.Dispose(); + } /// protected override async Task OnInitializedAsync() @@ -124,12 +164,29 @@ } IsSpamOk = IsSpamOkDomain(EmailAddress); + // Set up visibility change detection + await JsInteropService.RegisterVisibilityCallback(DotNetObjectReference.Create(this)); + // Only enable auto-refresh if the setting is enabled. if (DbService.Settings.AutoEmailRefresh) { - RefreshTimer = new Timer(2000); - RefreshTimer.Elapsed += async (sender, e) => await TimerRefresh(); - RefreshTimer.Start(); + await StartPolling(); + } + } + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!ShowComponent) + { + return; + } + + if (firstRender) + { + await ManualRefresh(); } } @@ -146,25 +203,58 @@ IsSpamOk = IsSpamOkDomain(EmailAddress); } - /// - public void Dispose() + /// + /// Start the polling for new emails. + /// + /// Task. + private async Task StartPolling() { - RefreshTimer?.Dispose(); + PollingCancellationTokenSource?.Cancel(); + PollingCancellationTokenSource = new CancellationTokenSource(); + + try + { + while (!PollingCancellationTokenSource.Token.IsCancellationRequested) + { + if (IsPageVisible) + { + // Only auto refresh when the tab is visible. + await RefreshWithThrottling(); + await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, PollingCancellationTokenSource.Token); + } + } + } + catch (OperationCanceledException) + { + // Normal cancellation, ignore + } } - /// - protected override async Task OnAfterRenderAsync(bool firstRender) + /// + /// Refresh the emails with throttling to prevent multiple refreshes at the same time. + /// + /// + private async Task RefreshWithThrottling() { - await base.OnAfterRenderAsync(firstRender); - - if (!ShowComponent) + if (!await RefreshSemaphore.WaitAsync(0)) // Don't wait if a refresh is in progress { return; } - if (firstRender) + try { - await ManualRefresh(); + var timeSinceLastRefresh = DateTime.UtcNow - LastRefreshTime; + if (timeSinceLastRefresh.TotalMilliseconds < ACTIVE_TAB_REFRESH_INTERVAL) + { + return; + } + + await LoadRecentEmailsAsync(); + LastRefreshTime = DateTime.UtcNow; + } + finally + { + RefreshSemaphore.Release(); } } @@ -184,15 +274,10 @@ return Config.PrivateEmailDomains.Exists(x => email.EndsWith(x)); } - private async Task TimerRefresh() - { - IsRefreshing = true; - StateHasChanged(); - await LoadRecentEmailsAsync(); - IsRefreshing = false; - StateHasChanged(); - } - + /// + /// Manually refresh the emails. + /// + /// private async Task ManualRefresh() { IsLoading = true; @@ -202,6 +287,10 @@ StateHasChanged(); } + /// + /// (Re)load recent emails by making an API call to the server. + /// + /// Task. private async Task LoadRecentEmailsAsync() { if (!ShowComponent || EmailAddress is null) diff --git a/src/AliasVault.Client/Services/JsInteropService.cs b/src/AliasVault.Client/Services/JsInteropService.cs index d32645134..4fb16aa6e 100644 --- a/src/AliasVault.Client/Services/JsInteropService.cs +++ b/src/AliasVault.Client/Services/JsInteropService.cs @@ -9,6 +9,7 @@ namespace AliasVault.Client.Services; using System.Security.Cryptography; using System.Text.Json; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.JSInterop; /// @@ -237,6 +238,16 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) await jsRuntime.InvokeVoidAsync("window.scrollTo", 0, 0); } + /// + /// Registers a visibility callback which is invoked when the visibility of component changes in client. + /// + /// Component type. + /// DotNetObjectReference. + /// Task. + public async Task RegisterVisibilityCallback(DotNetObjectReference objRef) + where TComponent : class => + await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef); + /// /// Represents the result of a WebAuthn get credential operation. /// diff --git a/src/AliasVault.Client/wwwroot/js/utilities.js b/src/AliasVault.Client/wwwroot/js/utilities.js index 4c4246128..e9ee0d83b 100644 --- a/src/AliasVault.Client/wwwroot/js/utilities.js +++ b/src/AliasVault.Client/wwwroot/js/utilities.js @@ -298,3 +298,9 @@ async function createWebAuthnCredentialAndDeriveKey(username) { return { Error: "WEBAUTHN_CREATE_ERROR", Message: createError.message }; } } + +window.registerVisibilityCallback = function (dotnetHelper) { + document.addEventListener("visibilitychange", function () { + dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden); + }); +};