diff --git a/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs b/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs index 49b92220b..da4c1ffce 100644 --- a/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs +++ b/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs @@ -25,5 +25,6 @@ namespace BTCPayServer.Abstractions.Contracts public string Body { get; set; } public string ActionLink { get; set; } public bool Seen { get; set; } + public string StoreId { get; set; } } } diff --git a/BTCPayServer.Data/Data/NotificationData.cs b/BTCPayServer.Data/Data/NotificationData.cs index 3e099980f..154c87357 100644 --- a/BTCPayServer.Data/Data/NotificationData.cs +++ b/BTCPayServer.Data/Data/NotificationData.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel.DataAnnotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; namespace BTCPayServer.Data { @@ -23,7 +22,6 @@ namespace BTCPayServer.Data public byte[] Blob { get; set; } public string Blob2 { get; set; } - internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) { builder.Entity() diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5c371963c..5408bb5f4 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2461,7 +2461,7 @@ namespace BTCPayServer.Tests var ctrl = acc.GetController(); var newVersion = MockVersionFetcher.MOCK_NEW_VERSION; - var vm = Assert.IsType( + var vm = Assert.IsType( Assert.IsType(await ctrl.Index()).Model); Assert.True(vm.Skip == 0); diff --git a/BTCPayServer/Controllers/UINotificationsController.cs b/BTCPayServer/Controllers/UINotificationsController.cs index e5d6dce30..02f83e18e 100644 --- a/BTCPayServer/Controllers/UINotificationsController.cs +++ b/BTCPayServer/Controllers/UINotificationsController.cs @@ -1,17 +1,13 @@ using System; using System.Linq; -using System.Net.WebSockets; -using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Models.NotificationViewModels; -using BTCPayServer.Security; -using BTCPayServer.Services; using BTCPayServer.Services.Notifications; -using BTCPayServer.Services.Notifications.Blobs; +using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -23,35 +19,59 @@ namespace BTCPayServer.Controllers [Route("notifications/{action:lowercase=Index}")] public class UINotificationsController : Controller { + private readonly ApplicationDbContextFactory _factory; + private readonly StoreRepository _storeRepo; private readonly UserManager _userManager; private readonly NotificationManager _notificationManager; public UINotificationsController( + StoreRepository storeRepo, UserManager userManager, - NotificationManager notificationManager) + NotificationManager notificationManager, + ApplicationDbContextFactory factory) { + _storeRepo = storeRepo; _userManager = userManager; _notificationManager = notificationManager; + _factory = factory; } [HttpGet] - public async Task Index(int skip = 0, int count = 50, int timezoneOffset = 0) + public async Task Index(NotificationIndexViewModel model = null) { + model ??= new NotificationIndexViewModel { Skip = 0 }; + var timezoneOffset = model.TimezoneOffset ?? 0; + model.Status ??= "Unread"; + ViewBag.Status = model.Status; if (!ValidUserClaim(out var userId)) return RedirectToAction("Index", "UIHome"); - var res = await _notificationManager.GetNotifications(new NotificationsQuery() - { - Skip = skip, - Take = count, - UserId = userId - }); + var stores = await _storeRepo.GetStoresByUserId(userId); + model.Stores = stores.Where(store => !store.Archived).OrderBy(s => s.StoreName).ToList(); - var model = new IndexViewModel() { Skip = skip, Count = count, Items = res.Items, Total = res.Count }; + + await using var dbContext = _factory.CreateContext(); + + var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}"; + var fs = new SearchString(searchTerm, timezoneOffset); + model.Search = fs; + + var res = await _notificationManager.GetNotifications(new NotificationsQuery + { + Skip = model.Skip, + Take = model.Count, + UserId = userId, + SearchText = model.SearchText, + Type = fs.GetFilterArray("type"), + Stores = fs.GetFilterArray("store"), + Seen = model.Status == "Unread" ? false : null + }); + model.Items = res.Items; return View(model); } + [HttpPost] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)] public async Task FlipRead(string id) diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index a92c2cc2d..f966f85a9 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -237,7 +237,7 @@ namespace BTCPayServer.HostedServices if (InvoiceEventNotification.HandlesEvent(b.Name)) { await _notificationSender.SendNotification(new StoreScope(b.Invoice.StoreId), - new InvoiceEventNotification(b.Invoice.Id, b.Name)); + new InvoiceEventNotification(b.Invoice.Id, b.Name, b.Invoice.StoreId)); } if (b.Name == InvoiceEvent.Created) { diff --git a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs index 4b08a1986..6365e0596 100644 --- a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs +++ b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs @@ -1,12 +1,20 @@ using System.Collections.Generic; using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Data; namespace BTCPayServer.Models.NotificationViewModels { public class IndexViewModel : BasePagingViewModel { - public List Items { get; set; } - + public List Items { get; set; } = []; + public string SearchText { get; set; } + public string Status { get; set; } + public SearchString Search { get; set; } public override int CurrentPageCount => Items.Count; } + + public class NotificationIndexViewModel : IndexViewModel + { + public List Stores { get; set; } + } } diff --git a/BTCPayServer/Services/Notifications/Blobs/ExternalPayoutTransactionNotification.cs b/BTCPayServer/Services/Notifications/Blobs/ExternalPayoutTransactionNotification.cs index c94c66e4e..6c0fcb75f 100644 --- a/BTCPayServer/Services/Notifications/Blobs/ExternalPayoutTransactionNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/ExternalPayoutTransactionNotification.cs @@ -36,6 +36,7 @@ namespace BTCPayServer.Services.Notifications.Blobs { vm.Identifier = notification.Identifier; vm.Type = notification.NotificationType; + vm.StoreId = notification.StoreId; vm.Body = "A payment that was made to an approved payout by an external wallet is waiting for your confirmation."; vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts), diff --git a/BTCPayServer/Services/Notifications/Blobs/InviteAcceptedNotification.cs b/BTCPayServer/Services/Notifications/Blobs/InviteAcceptedNotification.cs index 41701f343..8dfa325b3 100644 --- a/BTCPayServer/Services/Notifications/Blobs/InviteAcceptedNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/InviteAcceptedNotification.cs @@ -44,6 +44,7 @@ internal class InviteAcceptedNotification : BaseNotification { vm.Identifier = notification.Identifier; vm.Type = notification.NotificationType; + vm.StoreId = notification.StoreId; vm.Body = $"User {notification.UserEmail} accepted the invite to {notification.StoreName}."; vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIStoresController.StoreUsers), "UIStores", diff --git a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs index 2fe197a16..ab477ed2a 100644 --- a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs @@ -54,6 +54,7 @@ namespace BTCPayServer.Services.Notifications.Blobs } vm.Identifier = notification.Identifier; vm.Type = notification.NotificationType; + vm.StoreId = notification?.StoreId; vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = notification.InvoiceId }, _options.RootPath); @@ -64,10 +65,11 @@ namespace BTCPayServer.Services.Notifications.Blobs { } - public InvoiceEventNotification(string invoiceId, string invoiceEvent) + public InvoiceEventNotification(string invoiceId, string invoiceEvent, string storeId) { InvoiceId = invoiceId; Event = invoiceEvent; + StoreId = storeId; } public static bool HandlesEvent(string invoiceEvent) @@ -77,6 +79,7 @@ namespace BTCPayServer.Services.Notifications.Blobs public string InvoiceId { get; set; } public string Event { get; set; } + public string StoreId { get; set; } public override string Identifier => Event is null ? TYPE : Event.ToStringLowerInvariant(); public override string NotificationType => TYPE; } diff --git a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs index 719f7b749..8514684e2 100644 --- a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs @@ -36,6 +36,7 @@ namespace BTCPayServer.Services.Notifications.Blobs { vm.Identifier = notification.Identifier; vm.Type = notification.NotificationType; + vm.StoreId = notification.StoreId; vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch { PayoutState.AwaitingApproval => "A new payout is awaiting for approval", diff --git a/BTCPayServer/Services/Notifications/NotificationManager.cs b/BTCPayServer/Services/Notifications/NotificationManager.cs index e4ecf4583..0b5b62ab8 100644 --- a/BTCPayServer/Services/Notifications/NotificationManager.cs +++ b/BTCPayServer/Services/Notifications/NotificationManager.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Data; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; namespace BTCPayServer.Services.Notifications { @@ -28,7 +25,6 @@ namespace BTCPayServer.Services.Notifications _handlersByNotificationType = handlers.ToDictionary(h => h.NotificationType); } - public async Task<(List Items, int? Count)> GetSummaryNotifications(string userId, bool cachedOnly) { var cacheKey = GetNotificationsCacheId(userId); @@ -68,7 +64,7 @@ namespace BTCPayServer.Services.Notifications var queryables = GetNotificationsQueryable(dbContext, query); var items = (await queryables.withPaging.ToListAsync()).Select(ToViewModel).Where(model => model != null).ToList(); - + items = FilterNotifications(items, query); int? count = null; if (query.Seen is false) { @@ -134,6 +130,34 @@ namespace BTCPayServer.Services.Notifications return (queryable, queryable2); } + private List FilterNotifications(List notifications, NotificationsQuery query) + { + if (!string.IsNullOrEmpty(query.SearchText)) + { + notifications = notifications.Where(data => data.Body.Contains(query.SearchText)).ToList(); + } + if (query.Type?.Length > 0) + { + if (query.Type?.Length > 0) + { + if (query.Type.Contains("userupdate")) + { + notifications = notifications.Where(n => n.Type.Equals("inviteaccepted", StringComparison.OrdinalIgnoreCase) || + n.Type.Equals("userapproval", StringComparison.OrdinalIgnoreCase)).ToList(); + } + else + { + notifications = notifications.Where(n => query.Type.Contains(n.Type, StringComparer.OrdinalIgnoreCase)).ToList(); + } + } + } + if (query.Stores?.Length > 0) + { + notifications = notifications.Where(n => !string.IsNullOrEmpty(n.StoreId) && query.Stores.Contains(n.StoreId, StringComparer.OrdinalIgnoreCase)).ToList(); + } + return notifications; + } + public async Task> ToggleSeen(NotificationsQuery notificationsQuery, bool? setSeen) { await using var dbContext = _factory.CreateContext(); @@ -195,5 +219,8 @@ namespace BTCPayServer.Services.Notifications public int? Skip { get; set; } public int? Take { get; set; } public bool? Seen { get; set; } + public string SearchText { get; set; } + public string[] Type { get; set; } + public string[] Stores { get; set; } } } diff --git a/BTCPayServer/Services/Notifications/NotificationSender.cs b/BTCPayServer/Services/Notifications/NotificationSender.cs index 0ad148eff..c02019206 100644 --- a/BTCPayServer/Services/Notifications/NotificationSender.cs +++ b/BTCPayServer/Services/Notifications/NotificationSender.cs @@ -58,6 +58,11 @@ namespace BTCPayServer.Services.Notifications } } + public BaseNotification GetBaseNotification(NotificationData notificationData) + { + return notificationData.HasTypedBlob().GetBlob(); + } + private async Task GetUsers(INotificationScope scope, string notificationIdentifier) { await using var ctx = _contextFactory.CreateContext(); diff --git a/BTCPayServer/Views/UINotifications/Index.cshtml b/BTCPayServer/Views/UINotifications/Index.cshtml index 8a2071e01..a376b9e4a 100644 --- a/BTCPayServer/Views/UINotifications/Index.cshtml +++ b/BTCPayServer/Views/UINotifications/Index.cshtml @@ -1,38 +1,137 @@ -@model BTCPayServer.Models.NotificationViewModels.IndexViewModel +@model BTCPayServer.Models.NotificationViewModels.NotificationIndexViewModel @{ ViewData["Title"] = "Notifications"; + string status = ViewBag.Status; + var statusFilterCount = CountArrayFilter("type"); + var storesFilterCount = CountArrayFilter("store"); } - +@functions +{ + private int CountArrayFilter(string type) => + Model.Search.ContainsFilter(type) ? Model.Search.GetFilterArray(type).Length : 0; -
+ private bool HasArrayFilter(string type, string key = null) => + Model.Search.ContainsFilter(type) && (key is null || Model.Search.GetFilterArray(type).Contains(key)); +} + +@section PageHeadContent +{ + +} + + + +
+ + + + +
+ +
+ + + + + + +
+ + + @if (Model.Items.Count > 0) {
@if (Model.Items.Any()) { -
+
+ - - + @@ -40,30 +139,31 @@ - + @foreach (var item in Model.Items) { @@ -71,19 +171,23 @@ - + + @@ -103,20 +207,15 @@ else

} + + +
Message
Date -
MessageActions
-
-
- 0 - selected -
+
+
- - + @if (Model.Status == "Unread") + { + + }
+
+
+ 0 + selected +
@item.Created.ToBrowserDate() @item.Body @item.Created.ToTimeAgo()
- @if (!string.IsNullOrEmpty(item.ActionLink)) { Details } + @if (!item.Seen) + { + + }