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 @@
- @if (RefreshTimer is not null)
+ @if (DbService.Settings.AutoEmailRefresh)
{
}
@@ -56,27 +56,27 @@
-
- |
- Subject
- |
-
- Date & Time
- |
-
+
+ |
+ Subject
+ |
+
+ Date & Time
+ |
+
- @foreach (var mail in MailboxEmails)
- {
-
- |
- OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...
- |
-
- OpenEmail(mail.Id)">@mail.DateSystem
- |
-
- }
+ @foreach (var mail in MailboxEmails)
+ {
+
+ |
+ OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...
+ |
+
+ OpenEmail(mail.Id)">@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);
+ });
+};