Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
NotificationsViewModel.cs
1using System;
3using System.Collections.ObjectModel;
4using System.Globalization;
5using System.Linq;
6using System.Threading;
7using System.Threading.Tasks;
8using CommunityToolkit.Mvvm.ComponentModel;
9using CommunityToolkit.Mvvm.Input;
10using Microsoft.Maui.ApplicationModel;
15
17{
22 {
23 private readonly INotificationServiceV2 notificationService;
24 private readonly ObservableTask<int> notificationsLoader;
25 private readonly List<NotificationRecord> loadedRecords = new List<NotificationRecord>();
26 private readonly int batchSize = 15;
27 private int loadedCount;
28 private bool suppressNotificationReload;
29
35 {
36 this.notificationService = NotificationService;
37 this.Items = new ObservableCollection<NotificationListItem>();
38 this.HasMore = 0;
39 this.notificationsLoader = new ObservableTaskBuilder<int>()
40 .Named("Notifications Loader")
41 .AutoStart(false)
42 .UseTaskRun(true)
43 .Run(async ctx => await this.LoadBatchAsync(ctx.IsRefreshing, ctx.CancellationToken))
44 .Build();
45 }
46
50 public ObservableCollection<NotificationListItem> Items { get; }
51
55 public ObservableTask<int> NotificationsLoader => this.notificationsLoader;
56
60 [ObservableProperty]
61 private string searchText = string.Empty;
62
66 [ObservableProperty]
67 private bool showUnreadOnly;
68
72 [ObservableProperty]
73 private int hasMore;
74
78 [RelayCommand]
79 private void SetUnread()
80 {
81 this.ShowUnreadOnly = true;
82 this.ApplyFilters();
83 }
84
88 [RelayCommand]
89 private void SetAll()
90 {
91 this.ShowUnreadOnly = false;
92 this.ApplyFilters();
93 }
94
98 [RelayCommand]
99 private void ToggleUnreadFilter()
100 {
101 this.ShowUnreadOnly = !this.ShowUnreadOnly;
102 this.ApplyFilters();
103 }
104
108 [RelayCommand(AllowConcurrentExecutions = false)]
109 private Task LoadMoreNotifications()
110 {
111 if (this.HasMore == -1)
112 return Task.CompletedTask;
113
114 this.notificationsLoader.Refresh();
115
116 return Task.CompletedTask;
117 }
118
122 [RelayCommand]
123 private async Task ClearAllAsync()
124 {
125 this.suppressNotificationReload = true;
126 try
127 {
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;
132 this.HasMore = 0;
133 this.ApplyFilters();
134 this.notificationsLoader.Run();
135 await this.notificationsLoader.WaitAllAsync();
136 }
137 finally
138 {
139 this.suppressNotificationReload = false;
140 }
141 }
142
147 [RelayCommand]
148 private async Task OpenNotificationAsync(NotificationListItem Item)
149 {
150 try
151 {
152 this.suppressNotificationReload = true;
153 await this.notificationService.ConsumeAsync(Item.Id, CancellationToken.None);
154 this.UpdateRecordState(Item.Id, NotificationState.Consumed, true);
155 }
156 catch (Exception ex)
157 {
158 ServiceRef.LogService.LogException(ex);
159 }
160 finally
161 {
162 this.suppressNotificationReload = false;
163 }
164 this.ApplyFilters();
165 }
166
171 [RelayCommand]
172 private async Task MarkReadAsync(NotificationListItem Item)
173 {
174 try
175 {
176 this.suppressNotificationReload = true;
177 await this.notificationService.MarkReadAsync(Item.Id, CancellationToken.None);
178 this.UpdateRecordState(Item.Id, NotificationState.Read, false);
179 }
180 catch (Exception ex)
181 {
182 ServiceRef.LogService.LogException(ex);
183 }
184 finally
185 {
186 this.suppressNotificationReload = false;
187 }
188 this.ApplyFilters();
189 }
190
192 public override async Task OnAppearingAsync()
193 {
194 this.notificationService.OnNotificationAdded += this.OnNotificationAddedAsync;
195 this.notificationsLoader.Run();
196 await this.notificationsLoader.WaitAllAsync();
197 await base.OnAppearingAsync();
198 }
199
201 public override async Task OnDisappearingAsync()
202 {
203 this.notificationService.OnNotificationAdded -= this.OnNotificationAddedAsync;
204 await base.OnDisappearingAsync();
205 }
206
207 partial void OnSearchTextChanged(string Value)
208 {
209 this.ApplyFilters();
210 }
211
212 private async Task LoadBatchAsync(bool IsRefresh, CancellationToken CancellationToken)
213 {
214 CancellationToken.ThrowIfCancellationRequested();
215
216 if (!IsRefresh)
217 {
218 this.loadedRecords.Clear();
219 this.loadedCount = 0;
220 }
221
223 {
224 States = this.ShowUnreadOnly ? new[] { NotificationState.New, NotificationState.Delivered } : null,
225 Limit = this.batchSize,
226 Skip = IsRefresh ? this.loadedCount : 0
227 };
228
229 IReadOnlyList<NotificationRecord> Records = await this.notificationService.GetAsync(Query, CancellationToken);
230
231 CancellationToken.ThrowIfCancellationRequested();
232
233 if (!IsRefresh)
234 this.loadedRecords.Clear();
235
236 this.UpsertRecords(Records);
237
238 CancellationToken.ThrowIfCancellationRequested();
239
240 int FetchedCount = Records.Count;
241 int NewHasMore = FetchedCount < this.batchSize ? -1 : 0;
242
243 if (CancellationToken.IsCancellationRequested)
244 {
245 return;
246 }
247
248 MainThread.BeginInvokeOnMainThread(() =>
249 {
250 if (CancellationToken.IsCancellationRequested)
251 {
252 return;
253 }
254
255 this.HasMore = NewHasMore;
256 });
257
258 this.ApplyFilters(CancellationToken);
259 }
260
261 private void UpsertRecords(IEnumerable<NotificationRecord> records)
262 {
263 foreach (NotificationRecord Record in records)
264 {
265 int Index = this.loadedRecords.FindIndex(r => string.Equals(r.Id, Record.Id, StringComparison.Ordinal));
266 if (Index >= 0)
267 {
268 this.loadedRecords[Index] = Record;
269 }
270 else
271 {
272 this.loadedRecords.Add(Record);
273 }
274 }
275
276 this.loadedCount = this.loadedRecords.Count;
277 }
278
279 private void RemoveRecords(IEnumerable<string> ids)
280 {
281 HashSet<string> IdSet = new HashSet<string>(ids);
282 this.loadedRecords.RemoveAll(Record => IdSet.Contains(Record.Id));
283 this.loadedCount = this.loadedRecords.Count;
284 }
285
286 private void UpdateRecordState(string id, NotificationState state, bool markConsumed)
287 {
288 NotificationRecord? Record = this.loadedRecords.FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.Ordinal));
289 if (Record is null)
290 return;
291
292 Record.State = state;
293
294 if (state == NotificationState.Read)
295 {
296 Record.ReadAt = DateTime.UtcNow;
297 }
298
299 if (markConsumed || state == NotificationState.Consumed)
300 {
301 Record.ConsumedAt = DateTime.UtcNow;
302 Record.OccurrenceCount = 1;
303 }
304
305 this.loadedCount = this.loadedRecords.Count;
306 }
307
308 private void ApplyFilters(CancellationToken CancellationToken = default)
309 {
310 if (CancellationToken.IsCancellationRequested)
311 {
312 return;
313 }
314
315 IEnumerable<NotificationRecord> Query = this.loadedRecords;
316
317 if (this.ShowUnreadOnly)
318 {
319 Query = Query.Where(Record => Record.State == NotificationState.New || Record.State == NotificationState.Delivered);
320 }
321
322 if (!string.IsNullOrWhiteSpace(this.SearchText))
323 {
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));
329 }
330
331 List<NotificationListItem> Filtered = Query
332 .OrderByDescending(Record => Record.TimestampCreated)
333 .Select(this.ToListItem)
334 .ToList();
335
336 if (CancellationToken.IsCancellationRequested)
337 {
338 return;
339 }
340
341 MainThread.BeginInvokeOnMainThread(() =>
342 {
343 if (CancellationToken.IsCancellationRequested)
344 {
345 return;
346 }
347
348 this.Items.Clear();
349 foreach (NotificationListItem Item in Filtered)
350 {
351 this.Items.Add(Item);
352 }
353 });
354 }
355
356 private async Task OnNotificationAddedAsync(object? Sender, NotificationRecordEventArgs Args)
357 {
358 if (this.suppressNotificationReload)
359 return;
360
361 await MainThread.InvokeOnMainThreadAsync(() => this.notificationsLoader.Run());
362 }
363
368 [RelayCommand]
369 private async Task DeleteNotificationAsync(NotificationListItem Item)
370 {
371 try
372 {
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;
377 }
378 catch (Exception ex)
379 {
380 ServiceRef.LogService.LogException(ex);
381 }
382 finally
383 {
384 this.suppressNotificationReload = false;
385 }
386
387 this.ApplyFilters();
388
389 if (this.HasMore == 0)
390 this.notificationsLoader.Refresh();
391 }
392
393 private NotificationListItem ToListItem(NotificationRecord record)
394 {
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
398 {
399 NotificationState.New or NotificationState.Delivered => "New",
400 NotificationState.Read => "Read",
401 NotificationState.Consumed => "Opened",
402 _ => string.Empty
403 };
404
405 return new NotificationListItem(record.Id, record.Title, record.Body, Channel, DateText, StateLabel, record.OccurrenceCount);
406 }
407
408 }
409}
Query options for retrieving notifications.
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.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
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...
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.
Definition: ImplTypes.g.cs:58
NotificationState
Notification lifecycle state.