From cb8cf9406d7ffcb5c4d55be20e7d177442586cd4 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 18 Oct 2026 12:49:52 +0100 Subject: [PATCH] feat(notifications): support dismissing notifications in PostIt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 14 ++ .../HomePageNotificationsTests.cs | 81 ++++++++ .../ViewModels/Layout/HomePageViewModel.cs | 39 +++- src/PostIt/PostIt/Views/Layout/HomePage.axaml | 11 +- src/Yavsc.Abstract/Constants.cs | 2 + .../Messaging/DismissedNotification.cs | 11 ++ .../NotificationsApiClient.cs | 30 ++- .../DismissClicksApiController.cs | 76 ++++++++ .../Controllers/DimissClicksApiController.cs | 174 ------------------ src/Yavsc.Org/Controllers/HomeController.cs | 30 +++ src/Yavsc.Org/wwwroot/js/site.js | 4 +- .../DismissClicksApiControllerTests.cs | 98 ++++++++++ 12 files changed, 389 insertions(+), 181 deletions(-) create mode 100644 src/Yavsc.Abstract/Messaging/DismissedNotification.cs create mode 100644 src/Yavsc.Api/Controllers/Administration/DismissClicksApiController.cs delete mode 100644 src/Yavsc.Org/Controllers/DimissClicksApiController.cs create mode 100644 src/test/Yavsc.Api.Tests/DismissClicksApiControllerTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f0bc6b9..1019d3e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,24 @@ avec lecture/pause, libération du lecteur à la fermeture et accès au navigateur en cas d'indisponibilité de LibVLC. Les autres fichiers et les vidéos sur mobile/navigateur s'ouvrent dans le navigateur système. +* [Yavsc.Api] Contrôleur `DismissClicksApiController` (déplacé depuis + Yavsc.Org) exposant des DTO `DismissedNotification` : + `GET api/v1/dismiss`, `POST api/v1/dismiss/click/{notificationId}` + (idempotent, 404 si la notification n'existe pas) et + `DELETE api/v1/dismiss/click/{notificationId}` ; authentification requise. +* [Yavsc.Api.Client] `NotificationsApiClient.DismissAsync`, + `GetDismissedAsync` et `RestoreAsync`. +* [PostIt] Bouton de fermeture sur chaque notification de l'accueil + qui la démet auprès de Yavsc.Api pour un utilisateur connecté, ou la + masque localement sans appel API pour un visiteur non connecté + (jusqu'au prochain rechargement des notifications). ### Changed * L'administration est maintenant restreinte aux emails configurés dans les paramétres applicatifs de Yavsc.Org (`appsettings-org.json`) +* [Yavsc.Org] L'API `api/v1/dimiss` est supprimée ; le site web démet ses + notifications via `POST /Home/DismissNotification/{id}` (base pour un + utilisateur connecté, session pour un visiteur anonyme). ## [1.0.8-rc25] - unstable diff --git a/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs b/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs index 667ea599a..3656f0d83 100644 --- a/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs +++ b/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs @@ -90,6 +90,22 @@ public class HomePageNotificationsTests session.Refresh(); Assert.False(transport.WasAnonymous); Assert.EndsWith("/notifications/me", transport.LastPath); + + var note = Note(3, "Dismiss me"); + vm.Notifications.Add(note); + Assert.True(vm.DismissNotificationCommand.CanExecute(note)); + transport.DismissResponse = Task.FromException(new HttpRequestException("Server unavailable")); + await vm.DismissNotificationCommand.ExecuteAsync(note); + Assert.Contains(note, vm.Notifications); + Assert.Contains("Server unavailable", vm.NotificationsError); + + transport.DismissResponse = Task.CompletedTask; + await vm.DismissNotificationCommand.ExecuteAsync(note); + Assert.Equal(HttpMethod.Post, transport.LastMethod); + Assert.Equal("https://business.example/api/v1/dismiss/click/3", transport.LastPath); + Assert.DoesNotContain(note, vm.Notifications); + Assert.Null(vm.NotificationsError); + var privateResponse = new TaskCompletionSource>(); transport.Response = privateResponse.Task; var privateLoad = vm.RefreshNotificationsAsync(); @@ -112,6 +128,42 @@ public class HomePageNotificationsTests } } + [Fact] + public async Task Anonymous_dismiss_removes_the_notification_without_an_api_call() + { + var transport = new NotificationTransport(); + var vm = CreateViewModel(transport); + vm.Notifications.Add(Note(1, "Keep")); + vm.Notifications.Add(Note(2, "Dismiss me")); + + Assert.True(vm.DismissNotificationCommand.CanExecute(vm.Notifications[1])); + Assert.False(vm.DismissNotificationCommand.CanExecute(null)); + + await vm.DismissNotificationCommand.ExecuteAsync(vm.Notifications[1]); + + Assert.Null(transport.LastMethod); + Assert.Null(transport.LastPath); + Assert.Equal(1, Assert.Single(vm.Notifications).Id); + Assert.Null(vm.NotificationsError); + } + + [Fact] + public async Task Anonymous_dismiss_of_the_last_notification_updates_the_empty_state() + { + var transport = new NotificationTransport(); + var vm = CreateViewModel(transport); + transport.Response = Task.FromResult(new List { Note(3, "Public") }); + await vm.RefreshNotificationsAsync(); + Assert.False(vm.HasNoNotifications); + + await vm.DismissNotificationCommand.ExecuteAsync(vm.Notifications[0]); + + Assert.Empty(vm.Notifications); + Assert.True(vm.HasNoNotifications); + Assert.Null(vm.NotificationsError); + Assert.Null(transport.LastMethod); + } + [AvaloniaFact] public async Task Home_page_loads_on_navigation_and_renders_wrapped_titles_and_bodies() { @@ -138,6 +190,12 @@ public class HomePageNotificationsTests var refresh = Assert.Single(page.GetVisualDescendants().OfType