3using System.Collections.ObjectModel;
4using System.Globalization;
7using System.Threading.Tasks;
8using CommunityToolkit.Mvvm.ComponentModel;
9using CommunityToolkit.Mvvm.Input;
10using Microsoft.Maui.ApplicationModel;
25 private readonly List<NotificationRecord> loadedRecords =
new List<NotificationRecord>();
26 private readonly
int batchSize = 15;
27 private int loadedCount;
28 private bool suppressNotificationReload;
37 this.Items =
new ObservableCollection<NotificationListItem>();
40 .Named(
"Notifications Loader")
43 .Run(async ctx => await this.LoadBatchAsync(ctx.IsRefreshing, ctx.CancellationToken))
50 public ObservableCollection<NotificationListItem>
Items {
get; }
61 private string searchText =
string.Empty;
67 private bool showUnreadOnly;
79 private void SetUnread()
81 this.ShowUnreadOnly =
true;
91 this.ShowUnreadOnly =
false;
99 private void ToggleUnreadFilter()
101 this.ShowUnreadOnly = !this.ShowUnreadOnly;
108 [RelayCommand(AllowConcurrentExecutions =
false)]
109 private Task LoadMoreNotifications()
111 if (this.HasMore == -1)
112 return Task.CompletedTask;
114 this.notificationsLoader.Refresh();
116 return Task.CompletedTask;
123 private async Task ClearAllAsync()
125 this.suppressNotificationReload =
true;
128 List<string> Ids = this.
Items.Select(Item => Item.Id).ToList();
129 await this.notificationService.DeleteAsync(Ids, CancellationToken.None);
130 this.RemoveRecords(Ids);
131 this.loadedCount = this.loadedRecords.Count;
134 this.notificationsLoader.Run();
135 await this.notificationsLoader.WaitAllAsync();
139 this.suppressNotificationReload =
false;
148 private async Task OpenNotificationAsync(NotificationListItem Item)
152 this.suppressNotificationReload =
true;
153 await this.notificationService.ConsumeAsync(Item.Id, CancellationToken.None);
162 this.suppressNotificationReload =
false;
172 private async Task MarkReadAsync(NotificationListItem Item)
176 this.suppressNotificationReload =
true;
177 await this.notificationService.MarkReadAsync(Item.Id, CancellationToken.None);
186 this.suppressNotificationReload =
false;
194 this.notificationService.OnNotificationAdded += this.OnNotificationAddedAsync;
195 this.notificationsLoader.Run();
196 await this.notificationsLoader.WaitAllAsync();
197 await base.OnAppearingAsync();
203 this.notificationService.OnNotificationAdded -= this.OnNotificationAddedAsync;
204 await base.OnDisappearingAsync();
207 partial
void OnSearchTextChanged(
string Value)
212 private async Task LoadBatchAsync(
bool IsRefresh, CancellationToken CancellationToken)
214 CancellationToken.ThrowIfCancellationRequested();
218 this.loadedRecords.Clear();
219 this.loadedCount = 0;
224 States = this.ShowUnreadOnly ?
new[] {
NotificationState.New, NotificationState.Delivered } :
null,
225 Limit = this.batchSize,
226 Skip = IsRefresh ? this.loadedCount : 0
229 IReadOnlyList<NotificationRecord> Records = await this.notificationService.GetAsync(Query, CancellationToken);
231 CancellationToken.ThrowIfCancellationRequested();
234 this.loadedRecords.Clear();
236 this.UpsertRecords(Records);
238 CancellationToken.ThrowIfCancellationRequested();
240 int FetchedCount = Records.Count;
241 int NewHasMore = FetchedCount < this.batchSize ? -1 : 0;
243 if (CancellationToken.IsCancellationRequested)
248 MainThread.BeginInvokeOnMainThread(() =>
250 if (CancellationToken.IsCancellationRequested)
255 this.HasMore = NewHasMore;
258 this.ApplyFilters(CancellationToken);
261 private void UpsertRecords(IEnumerable<NotificationRecord> records)
265 int Index = this.loadedRecords.FindIndex(r =>
string.Equals(r.Id, Record.
Id, StringComparison.Ordinal));
268 this.loadedRecords[Index] = Record;
272 this.loadedRecords.Add(Record);
276 this.loadedCount = this.loadedRecords.Count;
279 private void RemoveRecords(IEnumerable<string> ids)
281 HashSet<string> IdSet =
new HashSet<string>(ids);
282 this.loadedRecords.RemoveAll(Record => IdSet.Contains(Record.
Id));
283 this.loadedCount = this.loadedRecords.Count;
286 private void UpdateRecordState(
string id,
NotificationState state,
bool markConsumed)
288 NotificationRecord? Record = this.loadedRecords.FirstOrDefault(r =>
string.Equals(r.Id,
id, StringComparison.Ordinal));
292 Record.State = state;
296 Record.ReadAt = DateTime.UtcNow;
301 Record.ConsumedAt = DateTime.UtcNow;
302 Record.OccurrenceCount = 1;
305 this.loadedCount = this.loadedRecords.Count;
308 private void ApplyFilters(CancellationToken CancellationToken =
default)
310 if (CancellationToken.IsCancellationRequested)
315 IEnumerable<NotificationRecord> Query = this.loadedRecords;
317 if (this.ShowUnreadOnly)
322 if (!
string.IsNullOrWhiteSpace(this.SearchText))
324 string Term = this.SearchText.Trim();
325 Query = Query.Where(Record =>
326 (Record.
Title?.Contains(Term, StringComparison.OrdinalIgnoreCase) ??
false) ||
327 (Record.
Body?.Contains(Term, StringComparison.OrdinalIgnoreCase) ??
false) ||
328 (Record.
Channel?.Contains(Term, StringComparison.OrdinalIgnoreCase) ??
false));
331 List<NotificationListItem> Filtered = Query
333 .Select(this.ToListItem)
336 if (CancellationToken.IsCancellationRequested)
341 MainThread.BeginInvokeOnMainThread(() =>
343 if (CancellationToken.IsCancellationRequested)
349 foreach (NotificationListItem Item in Filtered)
351 this.
Items.Add(Item);
358 if (this.suppressNotificationReload)
361 await MainThread.InvokeOnMainThreadAsync(() => this.notificationsLoader.Run());
369 private async Task DeleteNotificationAsync(NotificationListItem Item)
373 this.suppressNotificationReload =
true;
374 await this.notificationService.DeleteAsync(
new[] { Item.Id }, CancellationToken.None);
375 this.RemoveRecords(
new[] { Item.Id });
376 this.loadedCount = this.loadedRecords.Count;
384 this.suppressNotificationReload =
false;
389 if (this.HasMore == 0)
390 this.notificationsLoader.Refresh();
395 string Channel = record.
Channel?.Trim() ??
string.Empty;
396 string DateText = record.
TimestampCreated.ToLocalTime().ToString(
"MMM d", CultureInfo.CurrentCulture);
397 string StateLabel = record.State
switch
399 NotificationState.New or NotificationState.Delivered =>
"New",
400 NotificationState.Read =>
"Read",
401 NotificationState.Consumed =>
"Opened",
Query options for retrieving notifications.
Event args for notification record events.
Persisted notification record.
string Channel
Gets or sets the channel identifier.
DateTime TimestampCreated
Gets or sets the timestamp when the notification was created.
string Id
Gets or sets the stable notification identifier.
int OccurrenceCount
Gets or sets how many times this notification intent has been observed.
NotificationState State
Gets or sets the notification state.
string? Body
Gets or sets the notification body.
string Title
Gets or sets the notification title.
Base class that references services in the app.
static ILogService LogService
Log service.
Provides a data-binding friendly mechanism to manage and report the status of asynchronous operations...
A base class for all view models, inheriting from the BindableObject. NOTE: using this class requir...
View model for notifications page.
override async Task OnDisappearingAsync()
Method called when view is disappearing from the screen.
ObservableCollection< NotificationListItem > Items
Notifications to display.
ObservableTask< int > NotificationsLoader
Loader task for batched notifications retrieval.
NotificationsViewModel(INotificationServiceV2 NotificationService)
Initializes a new instance of the NotificationsViewModel class.
override async Task OnAppearingAsync()
Method called when view is appearing on the screen.
Interface for the redesigned notification service.
NotificationState
Notification lifecycle state.