mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 09:54:30 +01:00
Rewrite the Notification dropdown with Blazor (#5325)
* Rewrite the Notification dropdown with Blazor * Test fix --------- Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
parent
e694568674
commit
44df8cf0c5
@ -1862,7 +1862,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
//offline/external payout test
|
||||
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click();
|
||||
s.Driver.FindElement(By.Id("NotificationsMarkAllAsSeen")).Click();
|
||||
|
||||
var newStore = s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
|
14
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
14
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace BTCPayServer.Blazor
|
||||
{
|
||||
public static class BlazorExtensions
|
||||
{
|
||||
public static bool IsPreRendering(this IJSRuntime runtime)
|
||||
{
|
||||
// The peculiar thing in prerender is that Blazor circuit isn't yet created, so we can't use JSInterop
|
||||
return !(bool)runtime.GetType().GetProperty("IsInitialized").GetValue(runtime);
|
||||
}
|
||||
}
|
||||
}
|
22
BTCPayServer/Blazor/Icon.razor
Normal file
22
BTCPayServer/Blazor/Icon.razor
Normal file
@ -0,0 +1,22 @@
|
||||
@using BTCPayServer.Abstractions.Extensions;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using Microsoft.AspNetCore.Hosting;
|
||||
@using Microsoft.AspNetCore.Mvc.Routing;
|
||||
@using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
@using Microsoft.AspNetCore.Mvc;
|
||||
@inject IFileVersionProvider FileVersionProvider
|
||||
@inject BTCPayServerOptions BTCPayServerOptions
|
||||
|
||||
<svg role="img" class="icon icon-@Symbol">
|
||||
<use href="@GetPathTo(Symbol)"></use>
|
||||
</svg>
|
||||
@code {
|
||||
public string GetPathTo(string symbol)
|
||||
{
|
||||
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||
return $"{rootPath}{versioned}#{Symbol}";
|
||||
}
|
||||
[Parameter]
|
||||
public string Symbol { get; set; }
|
||||
}
|
152
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
152
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
@ -0,0 +1,152 @@
|
||||
@using System.Security.Claims
|
||||
@using BTCPayServer.Abstractions.Contracts;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using BTCPayServer.Data;
|
||||
@using BTCPayServer.Services.Notifications;
|
||||
@using Microsoft.AspNetCore.Identity;
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider _AuthenticationStateProvider
|
||||
@inject NotificationManager _NotificationManager
|
||||
@inject UserManager<ApplicationUser> _UserManager
|
||||
@inject IJSRuntime _JSRuntime
|
||||
@inject LinkGenerator _LinkGenerator
|
||||
@inject BTCPayServerOptions _BTCPayServerOptions
|
||||
@inject EventAggregator _EventAggregator
|
||||
|
||||
<div id="Notifications">
|
||||
@if (UnseenCount == "0")
|
||||
{
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
|
||||
<Icon Symbol="notifications" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<Icon Symbol="notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
|
||||
</button>
|
||||
}
|
||||
@if (UnseenCount != "0" && Last5 is not null)
|
||||
{
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
|
||||
</div>
|
||||
<div id="NotificationsList" v-pre>
|
||||
@foreach (var n in Last5)
|
||||
{
|
||||
<a href="@NotificationUrl(n.Id)" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
||||
<div class="me-3">
|
||||
<Icon Symbol="@NotificationIcon(n.Identifier)" />
|
||||
</div>
|
||||
<div class="notification-item__content">
|
||||
<div class="text-start text-wrap">
|
||||
@n.Body
|
||||
</div>
|
||||
<div class="text-start d-flex">
|
||||
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<a href="@NotificationsUrl">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
string NotificationsUrl => _LinkGenerator.GetPathByAction("Index", "UINotifications", pathBase: _BTCPayServerOptions.RootPath);
|
||||
string NotificationUrl(string notificationId) => _LinkGenerator.GetPathByAction("NotificationPassThrough", "UINotifications", values: new { id = notificationId }, pathBase: _BTCPayServerOptions.RootPath);
|
||||
string UnseenCount;
|
||||
List<NotificationViewModel> Last5;
|
||||
IDisposable _EventAggregatorListener;
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (_JSRuntime.IsPreRendering())
|
||||
return;
|
||||
_EventAggregatorListener = _EventAggregator.Subscribe<UserNotificationsUpdatedEvent>((s, evt) =>
|
||||
{
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||
UpdateState(res);
|
||||
StateHasChanged();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() => _EventAggregatorListener?.Dispose();
|
||||
string SeenCount(int? count)
|
||||
{
|
||||
if (count is not int c)
|
||||
return "0";
|
||||
if (c >= NotificationManager.MaxUnseen)
|
||||
return $"{NotificationManager.MaxUnseen - 1}+";
|
||||
return c.ToString();
|
||||
}
|
||||
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
|
||||
{
|
||||
UnseenCount = SeenCount(res.Count);
|
||||
Last5 = res.Items;
|
||||
}
|
||||
protected async override Task OnParametersSetAsync()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
// For prerendering and first rendering, always use the cached value
|
||||
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: true);
|
||||
// If we forget to update the state here, the UI will flicker.
|
||||
// Because the first rendering will think there is 0 events, until the DB call ends and the second rendering happens.
|
||||
// By updating the state here, the first rendering will show the cached value until the second rendering happens
|
||||
UpdateState(res);
|
||||
// We don't want to block the pre-rendering, so we will render again when the costly request is over
|
||||
if (!_JSRuntime.IsPreRendering())
|
||||
{
|
||||
res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||
UpdateState(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
async Task<string>
|
||||
GetUserId()
|
||||
{
|
||||
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
if (!state.User.Identity.IsAuthenticated)
|
||||
return null;
|
||||
return _UserManager.GetUserId(state.User);
|
||||
}
|
||||
public async Task MarkAllAsSeen()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
await _NotificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true);
|
||||
UnseenCount = "0";
|
||||
}
|
||||
}
|
||||
private static string NotificationIcon(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"invoice_expired" => "notifications-invoice-failure",
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
"newversion" => "notifications-new-version",
|
||||
_ => "note"
|
||||
};
|
||||
}
|
||||
}
|
@ -5,3 +5,5 @@
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using BTCPayServer.Blazor
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
|
@ -1,31 +0,0 @@
|
||||
@using BTCPayServer.Views.Notifications
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
||||
|
||||
<div id="Notifications">
|
||||
@if (Model.UnseenCount > 0)
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<vc:icon symbol="notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
|
||||
</button>
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<form id="notificationsForm" asp-controller="UINotifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
|
||||
</form>
|
||||
</div>
|
||||
<partial name="Components/Notifications/List" model="Model"/>
|
||||
<div class="p-3">
|
||||
<a asp-controller="UINotifications" asp-action="Index">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="UINotifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
|
||||
<vc:icon symbol="notifications" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
@ -1,38 +0,0 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
||||
@functions {
|
||||
private static string NotificationIcon(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"invoice_expired" => "notifications-invoice-failure",
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
"newversion" => "notifications-new-version",
|
||||
_ => "note"
|
||||
};
|
||||
}
|
||||
}
|
||||
<div id="NotificationsList">
|
||||
@foreach (var n in Model.Last5)
|
||||
{
|
||||
<a asp-action="NotificationPassThrough" asp-controller="UINotifications" asp-route-id="@n.Id" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
||||
<div class="me-3">
|
||||
<vc:icon symbol="@NotificationIcon(n.Identifier)" />
|
||||
</div>
|
||||
<div class="notification-item__content">
|
||||
<div class="text-start text-wrap">
|
||||
@n.Body
|
||||
</div>
|
||||
<div class="text-start d-flex">
|
||||
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
@ -1,27 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.Notifications
|
||||
{
|
||||
public class Notifications : ViewComponent
|
||||
{
|
||||
private readonly NotificationManager _notificationManager;
|
||||
|
||||
private static readonly string[] _views = { "List", "Dropdown", "Recent" };
|
||||
|
||||
public Notifications(NotificationManager notificationManager)
|
||||
{
|
||||
_notificationManager = notificationManager;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appearance, string returnUrl)
|
||||
{
|
||||
var vm = await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal);
|
||||
vm.ReturnUrl = returnUrl;
|
||||
var viewName = _views.Contains(appearance) ? appearance : _views[0];
|
||||
return View(viewName, vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Components.Notifications
|
||||
{
|
||||
public class NotificationsViewModel
|
||||
{
|
||||
public string ReturnUrl { get; set; }
|
||||
public int UnseenCount { get; set; }
|
||||
public List<NotificationViewModel> Last5 { get; set; }
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
||||
|
||||
<div id="NotificationsRecent">
|
||||
@if (Model.Last5.Any())
|
||||
{
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">Recent Notifications</h4>
|
||||
<a asp-controller="UINotifications" asp-action="Index">View all</a>
|
||||
</div>
|
||||
<partial name="Components/Notifications/List" model="Model"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h4 class="mb-3">Notifications</h4>
|
||||
<p class="text-secondary mt-3">
|
||||
There are no recent unseen notifications.
|
||||
</p>
|
||||
}
|
||||
</div>
|
@ -23,85 +23,17 @@ namespace BTCPayServer.Controllers
|
||||
[Route("notifications/{action:lowercase=Index}")]
|
||||
public class UINotificationsController : Controller
|
||||
{
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly NotificationManager _notificationManager;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public UINotificationsController(BTCPayServerEnvironment env,
|
||||
NotificationSender notificationSender,
|
||||
public UINotificationsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
NotificationManager notificationManager,
|
||||
EventAggregator eventAggregator)
|
||||
NotificationManager notificationManager)
|
||||
{
|
||||
_env = env;
|
||||
_notificationSender = notificationSender;
|
||||
_userManager = userManager;
|
||||
_notificationManager = notificationManager;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetNotificationDropdownUI(string returnUrl)
|
||||
{
|
||||
return ViewComponent("Notifications", new { appearance = "Dropdown", returnUrl });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> SubscribeUpdates(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var userId = _userManager.GetUserId(User);
|
||||
var websocketHelper = new WebSocketHelper(websocket);
|
||||
IEventAggregatorSubscription subscription = null;
|
||||
try
|
||||
{
|
||||
subscription = _eventAggregator.SubscribeAsync<UserNotificationsUpdatedEvent>(async evt =>
|
||||
{
|
||||
if (evt.UserId == userId)
|
||||
{
|
||||
await websocketHelper.Send("update");
|
||||
}
|
||||
});
|
||||
|
||||
await websocketHelper.NextMessageAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
subscription?.Dispose();
|
||||
await websocketHelper.DisposeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
#if DEBUG
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateJunk(int x = 100, bool admin = true)
|
||||
{
|
||||
for (int i = 0; i < x; i++)
|
||||
{
|
||||
await _notificationSender.SendNotification(
|
||||
admin ? (NotificationScope)new AdminScope() : new UserScope(_userManager.GetUserId(User)),
|
||||
new JunkNotification());
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
#endif
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
|
@ -422,9 +422,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, ExternalPayoutTransactionNotification.Handler>();
|
||||
services.AddSingleton<IHostedService, DbMigrationsHostedService>();
|
||||
#if DEBUG
|
||||
services.AddSingleton<INotificationHandler, JunkNotification.Handler>();
|
||||
#endif
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.AddSingleton<IExplorerClientProvider, ExplorerClientProvider>(x =>
|
||||
x.GetRequiredService<ExplorerClientProvider>());
|
||||
|
@ -1,30 +0,0 @@
|
||||
#if DEBUG
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Services.Notifications.Blobs
|
||||
{
|
||||
internal class JunkNotification : BaseNotification
|
||||
{
|
||||
private const string TYPE = "junk";
|
||||
internal class Handler : NotificationHandler<JunkNotification>
|
||||
{
|
||||
public override string NotificationType => TYPE;
|
||||
public override (string identifier, string name)[] Meta
|
||||
{
|
||||
get
|
||||
{
|
||||
return new (string identifier, string name)[] { (TYPE, "Junk") };
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FillViewModel(JunkNotification notification, NotificationViewModel vm)
|
||||
{
|
||||
vm.Body = "All your junk r belong to us!";
|
||||
}
|
||||
}
|
||||
|
||||
public override string Identifier => NotificationType;
|
||||
public override string NotificationType => TYPE;
|
||||
}
|
||||
}
|
||||
#endif
|
@ -4,51 +4,46 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Components.Notifications;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Newtonsoft.Json;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
|
||||
namespace BTCPayServer.Services.Notifications
|
||||
{
|
||||
public class NotificationManager
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _factory;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly Dictionary<string, INotificationHandler> _handlersByNotificationType;
|
||||
|
||||
public NotificationManager(ApplicationDbContextFactory factory, UserManager<ApplicationUser> userManager,
|
||||
public NotificationManager(ApplicationDbContextFactory factory,
|
||||
IMemoryCache memoryCache, IEnumerable<INotificationHandler> handlers, EventAggregator eventAggregator)
|
||||
{
|
||||
_factory = factory;
|
||||
_userManager = userManager;
|
||||
_memoryCache = memoryCache;
|
||||
_eventAggregator = eventAggregator;
|
||||
_handlersByNotificationType = handlers.ToDictionary(h => h.NotificationType);
|
||||
}
|
||||
|
||||
private const int _cacheExpiryMs = 5000;
|
||||
|
||||
public async Task<NotificationsViewModel> GetSummaryNotifications(ClaimsPrincipal user)
|
||||
public async Task<(List<NotificationViewModel> Items, int? Count)> GetSummaryNotifications(string userId, bool cachedOnly)
|
||||
{
|
||||
var userId = _userManager.GetUserId(user);
|
||||
var cacheKey = GetNotificationsCacheId(userId);
|
||||
|
||||
if (cachedOnly)
|
||||
return _memoryCache.Get<(List<NotificationViewModel> Items, int? Count)>(cacheKey);
|
||||
return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
||||
{
|
||||
var resp = await GetNotifications(new NotificationsQuery
|
||||
var res = await GetNotifications(new NotificationsQuery
|
||||
{
|
||||
Seen = false,
|
||||
Skip = 0,
|
||||
Take = 5,
|
||||
UserId = userId
|
||||
});
|
||||
entry.SetAbsoluteExpiration(TimeSpan.FromMilliseconds(_cacheExpiryMs));
|
||||
var res = new NotificationsViewModel { Last5 = resp.Items, UnseenCount = resp.Count.Value };
|
||||
entry.Value = res;
|
||||
return res;
|
||||
});
|
||||
@ -67,7 +62,7 @@ namespace BTCPayServer.Services.Notifications
|
||||
{
|
||||
return $"notifications-{userId}";
|
||||
}
|
||||
|
||||
public const int MaxUnseen = 100;
|
||||
public async Task<(List<NotificationViewModel> Items, int? Count)> GetNotifications(NotificationsQuery query)
|
||||
{
|
||||
await using var dbContext = _factory.CreateContext();
|
||||
@ -80,6 +75,27 @@ namespace BTCPayServer.Services.Notifications
|
||||
{
|
||||
// Unseen notifications aren't likely to be too huge, so count should be fast
|
||||
count = await queryables.withoutPaging.CountAsync();
|
||||
if (count >= MaxUnseen)
|
||||
{
|
||||
// If we have too much unseen notifications, we don't want to show the exact count
|
||||
// because it would be too long to display, so we just show 99+
|
||||
// Then cleanup a bit the database by removing the oldest notifications, as it would be expensive to fetch every time
|
||||
if (count >= MaxUnseen + (MaxUnseen / 2))
|
||||
{
|
||||
nextBatch:
|
||||
var seenToRemove = await queryables.withoutPaging.OrderByDescending(data => data.Created).Skip(MaxUnseen).Take(1000).ToListAsync();
|
||||
if (seenToRemove.Count > 0)
|
||||
{
|
||||
foreach (var seen in seenToRemove)
|
||||
{
|
||||
seen.Seen = true;
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
goto nextBatch;
|
||||
}
|
||||
}
|
||||
count = MaxUnseen;
|
||||
}
|
||||
}
|
||||
return (Items: items, Count: count);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
<vc:store-selector />
|
||||
@if (_signInManager.IsSignedIn(User))
|
||||
{
|
||||
<vc:notifications appearance="Dropdown" return-url="@notificationsReturnUrl" />
|
||||
<component type="typeof(BTCPayServer.Blazor.NotificationsDropDown)" render-mode="ServerPrerendered" />
|
||||
}
|
||||
</div>
|
||||
<vc:main-nav />
|
||||
@ -84,27 +84,6 @@
|
||||
var tmpl = document.getElementById("badUrl");
|
||||
mainContent.prepend(tmpl.content.cloneNode(true));
|
||||
}
|
||||
if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
|
||||
const { host, protocol } = window.location;
|
||||
var wsUri = "@_linkGenerator.GetPathByAction("SubscribeUpdates", "UINotifications", pathBase: Context.Request.PathBase)";
|
||||
wsUri = (protocol === "https:" ? "wss:" : "ws:") + "//" + host + wsUri;
|
||||
const newDataEndpoint = "@_linkGenerator.GetPathByAction("GetNotificationDropdownUI", "UINotifications", pathBase: Context.Request.PathBase, values: new { returnUrl = notificationsReturnUrl })";
|
||||
try {
|
||||
socket = new WebSocket(wsUri);
|
||||
socket.onmessage = e => {
|
||||
if (e.data === "ping") return;
|
||||
$.get(newDataEndpoint, data => {
|
||||
$("#Notifications").replaceWith($(data));
|
||||
});
|
||||
};
|
||||
socket.onerror = e => {
|
||||
console.error("Error while connecting to websocket for notifications (callback)", e);
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Error while connecting to websocket for notifications", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
</main>
|
||||
|
Loading…
Reference in New Issue
Block a user