Refactor RecentEmails component to only load emails when app is visible (#426)

This commit is contained in:
Leendert de Borst
2024-11-26 18:33:35 +01:00
parent 5b58418e57
commit c73c41ca06
3 changed files with 149 additions and 43 deletions

View File

@@ -26,7 +26,7 @@
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
</div>
<div class="flex justify-end items-center space-x-2">
@if (RefreshTimer is not null)
@if (DbService.Settings.AutoEmailRefresh)
{
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="Auto-refresh enabled"></div>
}
@@ -56,27 +56,27 @@
<div class="overflow-hidden shadow sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
Subject
</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
Date &amp; Time
</th>
</tr>
<tr>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
Subject
</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
Date &amp; Time
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
@foreach (var mail in MailboxEmails)
{
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</span>
</td>
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
</td>
</tr>
}
@foreach (var mail in MailboxEmails)
{
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</span>
</td>
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
</td>
</tr>
}
</tbody>
</table>
</div>
@@ -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;
/// <summary>
/// Callback invoked by JavaScript when the page visibility changes.
/// </summary>
/// <param name="isVisible">Boolean whether the page is visible or not.</param>
/// <returns>Task.</returns>
[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();
}
/// <inheritdoc />
public void Dispose()
{
PollingCancellationTokenSource?.Cancel();
PollingCancellationTokenSource?.Dispose();
RefreshSemaphore.Dispose();
}
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public void Dispose()
/// <summary>
/// Start the polling for new emails.
/// </summary>
/// <returns>Task.</returns>
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
}
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
/// <summary>
/// Refresh the emails with throttling to prevent multiple refreshes at the same time.
/// </summary>
/// <returns></returns>
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();
}
/// <summary>
/// Manually refresh the emails.
/// </summary>
/// <returns></returns>
private async Task ManualRefresh()
{
IsLoading = true;
@@ -202,6 +287,10 @@
StateHasChanged();
}
/// <summary>
/// (Re)load recent emails by making an API call to the server.
/// </summary>
/// <returns>Task.</returns>
private async Task LoadRecentEmailsAsync()
{
if (!ShowComponent || EmailAddress is null)

View File

@@ -9,6 +9,7 @@ namespace AliasVault.Client.Services;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.JSInterop;
/// <summary>
@@ -237,6 +238,16 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
await jsRuntime.InvokeVoidAsync("window.scrollTo", 0, 0);
}
/// <summary>
/// Registers a visibility callback which is invoked when the visibility of component changes in client.
/// </summary>
/// <typeparam name="TComponent">Component type.</typeparam>
/// <param name="objRef">DotNetObjectReference.</param>
/// <returns>Task.</returns>
public async Task RegisterVisibilityCallback<TComponent>(DotNetObjectReference<TComponent> objRef)
where TComponent : class =>
await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef);
/// <summary>
/// Represents the result of a WebAuthn get credential operation.
/// </summary>

View File

@@ -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);
});
};