Refresh UI notifications automatically on update (#1680)

* Refresh UI notifications automatically on update

* make notif timeago live

* pass cancellation token

* Update InvoiceEventData.cs
This commit is contained in:
Andrew Camilleri 2020-06-24 10:23:16 +02:00 committed by GitHub
parent 8230a408ac
commit dc5d8a6cb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 148 additions and 44 deletions

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

View file

@ -226,6 +226,7 @@
<ItemGroup>
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>

View file

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
@ -14,6 +15,22 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public class NotificationsDropdown : ViewComponent
{
private readonly NotificationManager _notificationManager;
public NotificationsDropdown(NotificationManager notificationManager)
{
_notificationManager = notificationManager;
}
public async Task<IViewComponentResult> InvokeAsync(int noOfEmployee)
{
return View(await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal));
}
}
[BitpayAPIConstraint(false)]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("[controller]/[action]")]
@ -24,18 +41,63 @@ namespace BTCPayServer.Controllers
private readonly NotificationSender _notificationSender;
private readonly UserManager<ApplicationUser> _userManager;
private readonly NotificationManager _notificationManager;
private readonly EventAggregator _eventAggregator;
public NotificationsController(BTCPayServerEnvironment env,
ApplicationDbContext db,
NotificationSender notificationSender,
UserManager<ApplicationUser> userManager,
NotificationManager notificationManager)
NotificationManager notificationManager,
EventAggregator eventAggregator)
{
_env = env;
_db = db;
_notificationSender = notificationSender;
_userManager = userManager;
_notificationManager = notificationManager;
_eventAggregator = eventAggregator;
}
[HttpGet]
public async Task<IActionResult> GetNotificationDropdownUI()
{
return ViewComponent("NotificationsDropdown");
}
[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.Subscribe<UserNotificationsUpdatedEvent>(async evt =>
{
if (evt.UserId == userId)
{
await websocketHelper.Send("update");
}
});
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(2000, cancellationToken);
}
}
finally
{
subscription?.Dispose();
await websocketHelper.DisposeAsync(CancellationToken.None);
}
return new EmptyResult();
}
[HttpGet]

View file

@ -37,7 +37,10 @@ namespace BTCPayServer.Services.Notifications.Blobs
NotificationViewModel vm)
{
var baseStr = $"Invoice {notification.InvoiceId.Substring(0, 5)}..";
vm.Body = $"{baseStr} {TextMapping[notification.Event]}";
if (TextMapping.ContainsKey(notification.Event))
{
vm.Body = $"{baseStr} {TextMapping[notification.Event]}";
}
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(InvoiceController.Invoice),
"Invoice",
new {invoiceId = notification.InvoiceId}, _options.RootPath);

View file

@ -20,15 +20,17 @@ namespace BTCPayServer.Services.Notifications
private readonly ApplicationDbContextFactory _factory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IMemoryCache _memoryCache;
private readonly EventAggregator _eventAggregator;
private readonly Dictionary<string, INotificationHandler> _handlersByNotificationType;
private readonly Dictionary<Type, INotificationHandler> _handlersByBlobType;
public NotificationManager(ApplicationDbContextFactory factory, UserManager<ApplicationUser> userManager,
IMemoryCache memoryCache, IEnumerable<INotificationHandler> handlers)
IMemoryCache memoryCache, IEnumerable<INotificationHandler> handlers, EventAggregator eventAggregator)
{
_factory = factory;
_userManager = userManager;
_memoryCache = memoryCache;
_eventAggregator = eventAggregator;
_handlersByNotificationType = handlers.ToDictionary(h => h.NotificationType);
_handlersByBlobType = handlers.ToDictionary(h => h.NotificationBlobType);
}
@ -52,6 +54,8 @@ namespace BTCPayServer.Services.Notifications
public void InvalidateNotificationCache(string userId)
{
_memoryCache.Remove(GetNotificationsCacheId(userId));
_eventAggregator.Publish(new UserNotificationsUpdatedEvent() {UserId = userId});
}
private static string GetNotificationsCacheId(string userId)

View file

@ -1,28 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Notifications
{
public class UserNotificationsUpdatedEvent
{
public string UserId { get; set; }
}
public class NotificationSender
{
private readonly ApplicationDbContextFactory _contextFactory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly EventAggregator _eventAggregator;
private readonly NotificationManager _notificationManager;
public NotificationSender(ApplicationDbContextFactory contextFactory, UserManager<ApplicationUser> userManager, EventAggregator eventAggregator, NotificationManager notificationManager)
public NotificationSender(ApplicationDbContextFactory contextFactory, UserManager<ApplicationUser> userManager,NotificationManager notificationManager)
{
_contextFactory = contextFactory;
_userManager = userManager;
_eventAggregator = eventAggregator;
_notificationManager = notificationManager;
}

View file

@ -0,0 +1,33 @@
@model BTCPayServer.Services.Notifications.NotificationSummaryViewModel
@if (Model.UnseenCount > 0)
{
<li class="nav-item dropdown" id="notifications-nav-item">
<a class="nav-link js-scroll-trigger" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" id="Notifications">
<i class="fa fa-bell"></i>
</a>
<span class="alerts-badge badge badge-pill badge-danger">@Model.UnseenCount</span>
<div class="dropdown-menu dropdown-menu-right text-center notification-items" aria-labelledby="navbarDropdown">
@foreach (var notif in Model.Last5)
{
<a asp-action="NotificationPassThrough" asp-controller="Notifications" asp-route-id="@notif.Id" class="dropdown-item border-bottom">
<div class="text-left" style="width: 200px; white-space:normal;">
@notif.Body
</div>
<div class="text-left">
<small class="text-muted" data-timeago-unixms="@notif.Created.ToUnixTimeMilliseconds()">@notif.Created.ToTimeAgo()</small>
</div>
</a>
}
<a class="dropdown-item text-info" asp-controller="Notifications" asp-action="Index">See All</a>
</div>
</li>
}
else
{
<li class="nav-item" id="notifications-nav-item">
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger" id="Notifications">
<i class="fa fa-bell"></i>
</a>
</li>
}

View file

@ -1,35 +1,27 @@
@inject BTCPayServer.Services.Notifications.NotificationManager notificationManager
<div></div>
@await Component.InvokeAsync("NotificationsDropdown")
<script>
@{
var notificationModel = await notificationManager.GetSummaryNotifications(User);
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var ws_uri = "@Url.Action("SubscribeUpdates", "Notifications", new {}, Context.Request.Scheme)".replace("http", "ws");
var newDataEndpoint = "@Url.Action("GetNotificationDropdownUI", "Notifications", new {}, Context.Request.Scheme)";
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
$.get(newDataEndpoint, function(data){
$("#notifications-nav-item").replaceWith($(data));
});
};
socket.onerror = function (e) {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
catch (e) {
console.error("Error while connecting to websocket for notifications", e);
}
}
@if (notificationModel.UnseenCount > 0)
{
<li class="nav-item dropdown">
<a class="nav-link js-scroll-trigger" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" id="Notifications">
<i class="fa fa-bell"></i>
</a>
<span class="alerts-badge badge badge-pill badge-danger">@notificationModel.UnseenCount</span>
<div class="dropdown-menu dropdown-menu-right text-center" aria-labelledby="navbarDropdown">
@foreach (var notif in notificationModel.Last5)
{
<a asp-action="NotificationPassThrough" asp-controller="Notifications" asp-route-id="@notif.Id" class="dropdown-item border-bottom">
<div class="text-left" style="width: 200px; white-space:normal;">
@notif.Body
</div>
<div class="text-left">
<small class="text-muted">@notif.Created.ToTimeAgo()</small>
</div>
</a>
}
<a class="dropdown-item text-info" asp-controller="Notifications" asp-action="Index">See All</a>
</div>
</li>
}
else
{
<li class="nav-item">
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger" id="Notifications"><i class="fa fa-bell"></i></a>
</li>
}
</script>

View file

@ -6,7 +6,18 @@
var dateString = localDate.toLocaleDateString() + " " + localDate.toLocaleTimeString();
$(this).text(dateString);
});
function updateTimeAgo(){
var timeagoElements = $("[data-timeago-unixms]");
timeagoElements.each(function () {
var elem = $(this);
elem.text(moment(elem.data("timeago-unixms")).fromNow());
});
setTimeout(updateTimeAgo, 1000);
}
updateTimeAgo();
// intializing date time pickers throughts website
$(".flatdtpicker").each(function () {
var element = $(this);