feat(notifications): support dismissing notifications in PostIt
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
b90f15333d
commit
cb8cf9406d
12 changed files with 389 additions and 181 deletions
|
|
@ -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<List<Notification>>();
|
||||
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<Notification> { 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<Button>(),
|
||||
b => b.Command == vm.RefreshNotificationsCommand);
|
||||
Assert.True(refresh.IsEnabled);
|
||||
var dismiss = Assert.Single(page.GetVisualDescendants().OfType<Button>(),
|
||||
b => b.Command == vm.DismissNotificationCommand);
|
||||
Assert.Same(vm.Notifications[0], dismiss.CommandParameter);
|
||||
Assert.Equal("Close", dismiss.Content);
|
||||
Assert.True(dismiss.IsVisible);
|
||||
Assert.True(dismiss.IsEnabled);
|
||||
Assert.Contains(page.GetVisualDescendants().OfType<ScrollViewer>(),
|
||||
s => s.Extent.Height > s.Viewport.Height);
|
||||
|
||||
|
|
@ -145,6 +203,10 @@ public class HomePageNotificationsTests
|
|||
transport.Response = Task.FromResult(new List<Notification> { Note(2, "Updated title") });
|
||||
await nav.PopAsync();
|
||||
Assert.Equal(2, Assert.Single(vm.Notifications).Id);
|
||||
await vm.DismissNotificationCommand.ExecuteAsync(vm.Notifications[0]);
|
||||
window.UpdateLayout();
|
||||
Assert.Empty(vm.Notifications);
|
||||
Assert.Null(transport.LastMethod);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -173,10 +235,29 @@ public class HomePageNotificationsTests
|
|||
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
LastMethod = method;
|
||||
if (method == HttpMethod.Post)
|
||||
{
|
||||
LastPath = path;
|
||||
WasAnonymous = false;
|
||||
return DismissAsync<T>(path);
|
||||
}
|
||||
Assert.Equal(HttpMethod.Get, method);
|
||||
return ReadAsync<T>(path, false);
|
||||
}
|
||||
|
||||
private async Task<T> DismissAsync<T>(string path)
|
||||
{
|
||||
await DismissResponse;
|
||||
return (T)(object)new DismissedNotification
|
||||
{
|
||||
NotificationId = long.Parse(path[(path.LastIndexOf('/') + 1)..])
|
||||
};
|
||||
}
|
||||
|
||||
public HttpMethod? LastMethod { get; private set; }
|
||||
public Task DismissResponse { get; set; } = Task.CompletedTask;
|
||||
|
||||
private async Task<T> ReadAsync<T>(string path, bool anonymous)
|
||||
{
|
||||
LastPath = path;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
|
|
@ -59,6 +60,9 @@ public class HomePageViewModel : ViewModelBase
|
|||
SessionStatus.PropertyChanged += OnSessionStatusChanged;
|
||||
RefreshNotificationsCommand = new AsyncRelayCommand(RefreshNotificationsAsync,
|
||||
() => _notificationsClient is not null);
|
||||
DismissNotificationCommand = new AsyncRelayCommand<Notification?>(DismissNotificationAsync,
|
||||
n => n is not null,
|
||||
AsyncRelayCommandOptions.AllowConcurrentExecutions);
|
||||
|
||||
OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync);
|
||||
OpenProviderRequests = new AsyncRelayCommand(OpenProviderRequestsAsync);
|
||||
|
|
@ -86,6 +90,31 @@ public class HomePageViewModel : ViewModelBase
|
|||
public IAsyncRelayCommand OpenProviderEstimates { get; }
|
||||
public IAsyncRelayCommand OpenMyFiles { get; }
|
||||
public IAsyncRelayCommand RefreshNotificationsCommand { get; }
|
||||
public IAsyncRelayCommand<Notification?> DismissNotificationCommand { get; }
|
||||
|
||||
public async Task DismissNotificationAsync(Notification? notification)
|
||||
{
|
||||
if (notification is null)
|
||||
return;
|
||||
NotificationsError = null;
|
||||
try
|
||||
{
|
||||
if (SessionStatus.IsLoggedIn)
|
||||
{
|
||||
if (_notificationsClient is null)
|
||||
throw new InvalidOperationException("Notifications client is not available.");
|
||||
await _notificationsClient.DismissAsync(notification.Id);
|
||||
}
|
||||
var shown = Notifications.FirstOrDefault(n => n.Id == notification.Id);
|
||||
if (shown is not null)
|
||||
Notifications.Remove(shown);
|
||||
OnPropertyChanged(nameof(HasNoNotifications));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NotificationsError = $"Impossible de masquer la notification : {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshNotificationsAsync()
|
||||
{
|
||||
|
|
@ -134,9 +163,15 @@ public class HomePageViewModel : ViewModelBase
|
|||
|
||||
// Logout can complete on a worker thread; collections and bindings belong to the UI thread.
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
_ = RefreshNotificationsAsync();
|
||||
OnLoginChanged();
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => _ = RefreshNotificationsAsync());
|
||||
Dispatcher.UIThread.Post(OnLoginChanged);
|
||||
}
|
||||
|
||||
private void OnLoginChanged()
|
||||
{
|
||||
DismissNotificationCommand.NotifyCanExecuteChanged();
|
||||
_ = RefreshNotificationsAsync();
|
||||
}
|
||||
|
||||
private async Task OpenActivitiesAsync()
|
||||
|
|
|
|||
|
|
@ -37,8 +37,15 @@
|
|||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding title}" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="{Binding title}" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||
Content="{Binding click_action, TargetNullValue=Fermer}"
|
||||
ToolTip.Tip="Masquer cette notification"
|
||||
Command="{Binding $parent[ItemsControl].((vm:HomePageViewModel)DataContext).DismissNotificationCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding body}" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
|
|
|||
Loading…
Reference in a new issue