diff --git a/BTCPayServer.Data/Data/ApplicationDbContext.cs b/BTCPayServer.Data/Data/ApplicationDbContext.cs index 802ffae8e..8fa72d6f8 100644 --- a/BTCPayServer.Data/Data/ApplicationDbContext.cs +++ b/BTCPayServer.Data/Data/ApplicationDbContext.cs @@ -11,15 +11,15 @@ namespace BTCPayServer.Data { public ApplicationDbContext CreateDbContext(string[] args) { - + var builder = new DbContextOptionsBuilder(); - + builder.UseSqlite("Data Source=temp.db"); - + return new ApplicationDbContext(builder.Options, true); } } - + public class ApplicationDbContext : IdentityDbContext { private readonly bool _designTime; @@ -37,90 +37,27 @@ namespace BTCPayServer.Data public DbSet PlannedTransactions { get; set; } public DbSet PayjoinLocks { get; set; } - - public DbSet Apps - { - get; set; - } - - public DbSet InvoiceEvents - { - get; set; - } - + public DbSet Apps { get; set; } + public DbSet InvoiceEvents { get; set; } public DbSet OffchainTransactions { get; set; } - - public DbSet HistoricalAddressInvoices - { - get; set; - } - - public DbSet PendingInvoices - { - get; set; - } - public DbSet RefundAddresses - { - get; set; - } - - public DbSet Payments - { - get; set; - } - - public DbSet PaymentRequests - { - get; set; - } - + public DbSet HistoricalAddressInvoices { get; set; } + public DbSet PendingInvoices { get; set; } + public DbSet RefundAddresses { get; set; } + public DbSet Payments { get; set; } + public DbSet PaymentRequests { get; set; } public DbSet Wallets { get; set; } public DbSet WalletTransactions { get; set; } + public DbSet Stores { get; set; } + public DbSet UserStore { get; set; } + public DbSet AddressInvoices { get; set; } + public DbSet Settings { get; set; } + public DbSet PairingCodes { get; set; } + public DbSet PairedSINData { get; set; } + public DbSet ApiKeys { get; set; } + public DbSet Files { get; set; } + public DbSet U2FDevices { get; set; } + public DbSet Notifications { get; set; } - public DbSet Stores - { - get; set; - } - - public DbSet UserStore - { - get; set; - } - - public DbSet AddressInvoices - { - get; set; - } - - public DbSet Settings - { - get; set; - } - - - public DbSet PairingCodes - { - get; set; - } - - public DbSet PairedSINData - { - get; set; - } - - public DbSet ApiKeys - { - get; set; - } - - public DbSet Files - { - get; set; - } - - - public DbSet U2FDevices { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -131,6 +68,9 @@ namespace BTCPayServer.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + NotificationData.OnModelCreating(builder); + + builder.Entity() .HasOne(o => o.StoreData) .WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade); @@ -164,12 +104,12 @@ namespace BTCPayServer.Data .HasOne(o => o.StoreData) .WithMany(i => i.APIKeys) .HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade); - + builder.Entity() .HasOne(o => o.User) .WithMany(i => i.APIKeys) .HasForeignKey(i => i.UserId).OnDelete(DeleteBehavior.Cascade); - + builder.Entity() .HasIndex(o => o.StoreId); @@ -240,8 +180,8 @@ namespace BTCPayServer.Data o.UniqueId #pragma warning restore CS0618 }); - - + + builder.Entity() .HasOne(o => o.StoreData) .WithMany(i => i.PaymentRequests) @@ -264,7 +204,7 @@ namespace BTCPayServer.Data builder.Entity() .HasOne(o => o.WalletData) .WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade); - + if (Database.IsSqlite() && !_designTime) { // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations diff --git a/BTCPayServer.Data/Data/ApplicationUser.cs b/BTCPayServer.Data/Data/ApplicationUser.cs index 6c9c28fbc..eab00670e 100644 --- a/BTCPayServer.Data/Data/ApplicationUser.cs +++ b/BTCPayServer.Data/Data/ApplicationUser.cs @@ -10,6 +10,7 @@ namespace BTCPayServer.Data // Add profile data for application users by adding properties to the ApplicationUser class public class ApplicationUser : IdentityUser { + public List Notifications { get; set; } public List UserStores { get; @@ -26,7 +27,7 @@ namespace BTCPayServer.Data get; set; } - + public List U2FDevices { get; set; } public List APIKeys { get; set; } } diff --git a/BTCPayServer.Data/Data/NotificationData.cs b/BTCPayServer.Data/Data/NotificationData.cs new file mode 100644 index 000000000..a54afb521 --- /dev/null +++ b/BTCPayServer.Data/Data/NotificationData.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Data +{ + public class NotificationData + { + [MaxLength(36)] + public string Id { get; set; } + public DateTimeOffset Created { get; set; } + [MaxLength(50)] + public string ApplicationUserId { get; set; } + public ApplicationUser ApplicationUser { get; set; } + [MaxLength(100)] + public string NotificationType { get; set; } + public bool Seen { get; set; } + public byte[] Blob { get; set; } + + internal static void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(o => o.ApplicationUser) + .WithMany(n => n.Notifications) + .HasForeignKey(k => k.ApplicationUserId).OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/BTCPayServer.Data/Migrations/20200614002524_AddNotificationDataEntity.cs b/BTCPayServer.Data/Migrations/20200614002524_AddNotificationDataEntity.cs new file mode 100644 index 000000000..8f6153ef4 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200614002524_AddNotificationDataEntity.cs @@ -0,0 +1,48 @@ +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200614002524_AddNotificationDataEntity")] + public partial class AddNotificationDataEntity : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(maxLength: 36, nullable: false), + Created = table.Column(nullable: false), + ApplicationUserId = table.Column(maxLength: 50, nullable: true), + NotificationType = table.Column(maxLength: 100, nullable: true), + Seen = table.Column(nullable: false), + Blob = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + table.ForeignKey( + name: "FK_Notifications_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_ApplicationUserId", + table: "Notifications", + column: "ApplicationUserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notifications"); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index 72c3b022f..3bc9b6310 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace BTCPayServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.1"); + .HasAnnotation("ProductVersion", "3.1.4"); modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => { @@ -243,6 +243,36 @@ namespace BTCPayServer.Migrations b.ToTable("InvoiceEvents"); }); + modelBuilder.Entity("BTCPayServer.Data.NotificationData", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(36); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("NotificationType") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Notifications"); + }); + modelBuilder.Entity("BTCPayServer.Data.OffchainTransactionData", b => { b.Property("Id") @@ -760,6 +790,14 @@ namespace BTCPayServer.Migrations .IsRequired(); }); + modelBuilder.Entity("BTCPayServer.Data.NotificationData", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("Notifications") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => { b.HasOne("BTCPayServer.Data.StoreData", "StoreData") diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index afb30a3ba..fd251cf19 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1195,6 +1195,39 @@ namespace BTCPayServer.Tests } } + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanListNotifications() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.GrantAccess(true); + acc.RegisterDerivationScheme("BTC"); + + const string newVersion = "1.0.4.4"; + var ctrl = acc.GetController(); + var resp = await ctrl.Generate(newVersion); + + var vm = Assert.IsType( + Assert.IsType(ctrl.Index().Result).Model); + + Assert.True(vm.Skip == 0); + Assert.True(vm.Count == 50); + Assert.True(vm.Total == 1); + Assert.True(vm.Items.Count == 1); + + var fn = vm.Items.First(); + var now = DateTimeOffset.UtcNow; + Assert.True(fn.Created >= now.AddSeconds(-3)); + Assert.True(fn.Created <= now); + Assert.Equal($"New version {newVersion} released!", fn.Body); + Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink); + Assert.False(fn.Seen); + } + } + [Fact] [Trait("Integration", "Integration")] public async Task CanGetRates() diff --git a/BTCPayServer/Controllers/NotificationsController.cs b/BTCPayServer/Controllers/NotificationsController.cs new file mode 100644 index 000000000..ce86f5fa1 --- /dev/null +++ b/BTCPayServer/Controllers/NotificationsController.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Filters; +using BTCPayServer.HostedServices; +using BTCPayServer.Models.NotificationViewModels; +using BTCPayServer.Security; +using BTCPayServer.Services.Notifications; +using Google; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + [BitpayAPIConstraint(false)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public class NotificationsController : Controller + { + private readonly ApplicationDbContext _db; + private readonly NotificationSender _notificationSender; + private readonly UserManager _userManager; + + public NotificationsController(ApplicationDbContext db, NotificationSender notificationSender, UserManager userManager) + { + _db = db; + _notificationSender = notificationSender; + _userManager = userManager; + } + + [HttpGet] + public async Task Index(int skip = 0, int count = 50, int timezoneOffset = 0) + { + if (!ValidUserClaim(out var userId)) + return RedirectToAction("Index", "Home"); + + var model = new IndexViewModel() + { + Skip = skip, + Count = count, + Items = _db.Notifications + .OrderByDescending(a => a.Created) + .Skip(skip).Take(count) + .Where(a => a.ApplicationUserId == userId) + .Select(a => a.ViewModel()) + .ToList(), + Total = _db.Notifications.Where(a => a.ApplicationUserId == userId).Count() + }; + + return View(model); + } + + [HttpGet] + public async Task Generate(string version) + { + await _notificationSender.NoticeNewVersionAsync(version); + // waiting for event handler to catch up + await Task.Delay(500); + return RedirectToAction(nameof(Index)); + } + + [HttpPost] + public async Task FlipRead(string id) + { + if (ValidUserClaim(out var userId)) + { + var notif = _db.Notifications.Single(a => a.Id == id && a.ApplicationUserId == userId); + notif.Seen = !notif.Seen; + await _db.SaveChangesAsync(); + } + + return RedirectToAction(nameof(Index)); + } + + [HttpPost] + public async Task MassAction(string command, string[] selectedItems) + { + if (selectedItems != null) + { + if (command == "delete" && ValidUserClaim(out var userId)) + { + var toRemove = _db.Notifications.Where(a => a.ApplicationUserId == userId && selectedItems.Contains(a.Id)); + _db.Notifications.RemoveRange(toRemove); + await _db.SaveChangesAsync(); + + return RedirectToAction(nameof(Index)); + } + } + + return RedirectToAction(nameof(Index)); + } + + private bool ValidUserClaim(out string userId) + { + userId = _userManager.GetUserId(User); + return userId != null; + } + } +} diff --git a/BTCPayServer/Events/NotificationEvent.cs b/BTCPayServer/Events/NotificationEvent.cs new file mode 100644 index 000000000..ff2e3c68d --- /dev/null +++ b/BTCPayServer/Events/NotificationEvent.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Services.Notifications.Blobs; + +namespace BTCPayServer.Events +{ + internal class NotificationEvent + { + internal string[] ApplicationUserIds { get; set; } + internal BaseNotification Notification { get; set; } + } +} diff --git a/BTCPayServer/HostedServices/NotificationDbSaver.cs b/BTCPayServer/HostedServices/NotificationDbSaver.cs new file mode 100644 index 000000000..f307d2eec --- /dev/null +++ b/BTCPayServer/HostedServices/NotificationDbSaver.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Models.NotificationViewModels; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Memory; + +namespace BTCPayServer.HostedServices +{ + public class NotificationDbSaver : EventHostedServiceBase + { + private readonly ApplicationDbContextFactory _ContextFactory; + + public NotificationDbSaver(ApplicationDbContextFactory contextFactory, + EventAggregator eventAggregator) : base(eventAggregator) + { + _ContextFactory = contextFactory; + } + + protected override void SubscribeToEvents() + { + Subscribe(); + base.SubscribeToEvents(); + } + + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + var casted = (NotificationEvent)evt; + using (var db = _ContextFactory.CreateContext()) + { + foreach (var uid in casted.ApplicationUserIds) + { + var data = casted.Notification.ToData(uid); + db.Notifications.Add(data); + } + + await db.SaveChangesAsync(); + } + } + } + + public class NotificationManager + { + private readonly ApplicationDbContextFactory _factory; + private readonly UserManager _userManager; + private readonly IMemoryCache _memoryCache; + + public NotificationManager(ApplicationDbContextFactory factory, UserManager userManager, IMemoryCache memoryCache) + { + _factory = factory; + _userManager = userManager; + _memoryCache = memoryCache; + } + + private const int _cacheExpiryMs = 5000; + public NotificationSummaryViewModel GetSummaryNotifications(ClaimsPrincipal user) + { + var userId = _userManager.GetUserId(user); + + if (_memoryCache.TryGetValue(userId, out var obj)) + return obj; + + var resp = FetchNotificationsFromDb(userId); + _memoryCache.Set(userId, resp, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMilliseconds(_cacheExpiryMs))); + + return resp; + } + + private NotificationSummaryViewModel FetchNotificationsFromDb(string userId) + { + var resp = new NotificationSummaryViewModel(); + using (var _db = _factory.CreateContext()) + { + resp.UnseenCount = _db.Notifications + .Where(a => a.ApplicationUserId == userId && !a.Seen) + .Count(); + + if (resp.UnseenCount > 0) + { + try + { + resp.Last5 = _db.Notifications + .Where(a => a.ApplicationUserId == userId && !a.Seen) + .OrderByDescending(a => a.Created) + .Take(5) + .Select(a => a.ViewModel()) + .ToList(); + } + catch (System.IO.InvalidDataException) + { + // invalid notifications that are not pkuzipable, burn them all + var notif = _db.Notifications.Where(a => a.ApplicationUserId == userId); + _db.Notifications.RemoveRange(notif); + _db.SaveChanges(); + + resp.UnseenCount = 0; + resp.Last5 = new List(); + } + } + else + { + resp.Last5 = new List(); + } + } + + return resp; + } + } + + public class NotificationSummaryViewModel + { + public int UnseenCount { get; set; } + public List Last5 { get; set; } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 4290b56be..2034936d0 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -46,6 +46,7 @@ using BTCPayServer.Security.Bitpay; using Serilog; using BTCPayServer.Security.GreenField; using BTCPayServer.Services.Labels; +using BTCPayServer.Services.Notifications; namespace BTCPayServer.Hosting { @@ -200,6 +201,10 @@ namespace BTCPayServer.Hosting services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs new file mode 100644 index 000000000..093b1cde0 --- /dev/null +++ b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Services.Notifications.Blobs; +using Newtonsoft.Json; + +namespace BTCPayServer.Models.NotificationViewModels +{ + public class IndexViewModel + { + public int Skip { get; set; } + public int Count { get; set; } + public int Total { get; set; } + public List Items { get; set; } + } + + public class NotificationViewModel + { + public string Id { get; set; } + public DateTimeOffset Created { get; set; } + public string Body { get; set; } + public string ActionLink { get; set; } + public bool Seen { get; set; } + } + + public static class NotificationViewModelExt + { + public static NotificationViewModel ViewModel(this NotificationData data) + { + var baseType = typeof(BaseNotification); + + var fullTypeName = baseType.FullName.Replace(nameof(BaseNotification), data.NotificationType, StringComparison.OrdinalIgnoreCase); + var parsedType = baseType.Assembly.GetType(fullTypeName); + + var casted = (BaseNotification)JsonConvert.DeserializeObject(ZipUtils.Unzip(data.Blob), parsedType); + var obj = new NotificationViewModel + { + Id = data.Id, + Created = data.Created, + Seen = data.Seen + }; + + casted.FillViewModel(ref obj); + + return obj; + } + } +} diff --git a/BTCPayServer/Services/Notifications/Blobs/BaseNotification.cs b/BTCPayServer/Services/Notifications/Blobs/BaseNotification.cs new file mode 100644 index 000000000..8d5c67699 --- /dev/null +++ b/BTCPayServer/Services/Notifications/Blobs/BaseNotification.cs @@ -0,0 +1,33 @@ +using System; +using BTCPayServer.Data; +using BTCPayServer.Models.NotificationViewModels; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Notifications.Blobs +{ + // Make sure to keep all Blob Notification classes in same namespace + // because of dependent initialization and parsing to view models logic + // IndexViewModel.cs#32 + internal abstract class BaseNotification + { + internal virtual string NotificationType { get { return GetType().Name; } } + + public NotificationData ToData(string applicationUserId) + { + var obj = JsonConvert.SerializeObject(this); + + var data = new NotificationData + { + Id = Guid.NewGuid().ToString(), + Created = DateTimeOffset.UtcNow, + ApplicationUserId = applicationUserId, + NotificationType = NotificationType, + Blob = ZipUtils.Zip(obj), + Seen = false + }; + return data; + } + + public abstract void FillViewModel(ref NotificationViewModel data); + } +} diff --git a/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs b/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs new file mode 100644 index 000000000..f6bab8d6d --- /dev/null +++ b/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs @@ -0,0 +1,19 @@ +using BTCPayServer.Data; +using BTCPayServer.Models.NotificationViewModels; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Notifications.Blobs +{ + internal class NewVersionNotification : BaseNotification + { + internal override string NotificationType => "NewVersionNotification"; + + public string Version { get; set; } + + public override void FillViewModel(ref NotificationViewModel vm) + { + vm.Body = $"New version {Version} released!"; + vm.ActionLink = $"https://github.com/btcpayserver/btcpayserver/releases/tag/v{Version}"; + } + } +} diff --git a/BTCPayServer/Services/Notifications/NotificationSender.cs b/BTCPayServer/Services/Notifications/NotificationSender.cs new file mode 100644 index 000000000..3a53b3cea --- /dev/null +++ b/BTCPayServer/Services/Notifications/NotificationSender.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Services.Notifications.Blobs; +using Microsoft.AspNetCore.Identity; + +namespace BTCPayServer.Services.Notifications +{ + public class NotificationSender + { + private readonly UserManager _userManager; + private readonly EventAggregator _eventAggregator; + + public NotificationSender(UserManager userManager, EventAggregator eventAggregator) + { + _userManager = userManager; + _eventAggregator = eventAggregator; + } + + internal async Task NoticeNewVersionAsync(string version) + { + var admins = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin); + var adminUids = admins.Select(a => a.Id).ToArray(); + var evt = new NotificationEvent + { + ApplicationUserIds = adminUids, + Notification = new NewVersionNotification + { + Version = version + } + }; + + _eventAggregator.Publish(evt); + } + } +} diff --git a/BTCPayServer/Views/Notifications/Index.cshtml b/BTCPayServer/Views/Notifications/Index.cshtml new file mode 100644 index 000000000..603bb21ba --- /dev/null +++ b/BTCPayServer/Views/Notifications/Index.cshtml @@ -0,0 +1,170 @@ +@model BTCPayServer.Models.NotificationViewModels.IndexViewModel +@{ + ViewData["Title"] = "Notifications"; +} + +
+
+ @if (TempData.HasStatusMessage()) + { +
+
+ +
+
+ } +
+
+

@ViewData["Title"]

+
+
+
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + + + @foreach (var item in Model.Items) + { + + + + + + + } + +
DateMessage 
+ + @item.Created.ToBrowserDate()@item.Body + @if (!String.IsNullOrEmpty(item.ActionLink)) + { + Action + } + else + { +   + } +
+ + + @{ + string ListItemsPage(int prevNext, int count) + { + var skip = Model.Skip; + if (prevNext == -1) + { + skip = Math.Max(0, Model.Skip - Model.Count); + } + else if (prevNext == 1) + { + skip = Model.Skip + count; + } + + var act = Url.Action("Index", new + { + skip = skip, + count = count, + }); + + return act; + } + } + +
+
+ +
+ +
+
+ + diff --git a/BTCPayServer/Views/Notifications/_ViewImports.cshtml b/BTCPayServer/Views/Notifications/_ViewImports.cshtml new file mode 100644 index 000000000..e02abfc9b --- /dev/null +++ b/BTCPayServer/Views/Notifications/_ViewImports.cshtml @@ -0,0 +1 @@ + diff --git a/BTCPayServer/Views/Shared/LayoutPartials/NotificationsNavItem.cshtml b/BTCPayServer/Views/Shared/LayoutPartials/NotificationsNavItem.cshtml new file mode 100644 index 000000000..c7caceb1b --- /dev/null +++ b/BTCPayServer/Views/Shared/LayoutPartials/NotificationsNavItem.cshtml @@ -0,0 +1,35 @@ +@inject BTCPayServer.HostedServices.NotificationManager notificationManager + +@{ + var notificationModel = notificationManager.GetSummaryNotifications(User); +} + +@if (notificationModel.UnseenCount > 0) +{ + +} +else +{ + +} diff --git a/BTCPayServer/Views/Shared/LayoutPartials/SyncModal.cshtml b/BTCPayServer/Views/Shared/LayoutPartials/SyncModal.cshtml new file mode 100644 index 000000000..e01fef5dd --- /dev/null +++ b/BTCPayServer/Views/Shared/LayoutPartials/SyncModal.cshtml @@ -0,0 +1,106 @@ +@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard + +@if (!dashboard.IsFullySynched()) +{ + + + + @**@ + + + + +} diff --git a/BTCPayServer/Views/Shared/SyncModal.cshtml b/BTCPayServer/Views/Shared/SyncModal.cshtml deleted file mode 100644 index ff7a892ed..000000000 --- a/BTCPayServer/Views/Shared/SyncModal.cshtml +++ /dev/null @@ -1,101 +0,0 @@ -@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard - - - -@**@ - - - diff --git a/BTCPayServer/Views/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml index 3d3daa94c..58cf15de1 100644 --- a/BTCPayServer/Views/Shared/_Layout.cshtml +++ b/BTCPayServer/Views/Shared/_Layout.cshtml @@ -2,7 +2,6 @@ @inject UserManager UserManager @inject RoleManager RoleManager @inject BTCPayServer.Services.BTCPayServerEnvironment env -@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard @inject BTCPayServer.HostedServices.CssThemeManager themeManager @@ -17,13 +16,17 @@ @@ -42,7 +45,7 @@