Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
NotificationService.cs
1using System.Diagnostics.CodeAnalysis;
2using Waher.Events;
6
8{
12 [Singleton]
14 {
15 private const int nrTypes = 4;
16
17 private readonly SortedDictionary<CaseInsensitiveString, List<NotificationEvent>>[] events;
18 private readonly LinkedList<ExpectedEvent> expected;
19
24 {
25 int i;
26
27 this.events = new SortedDictionary<CaseInsensitiveString, List<NotificationEvent>>[nrTypes];
28 this.expected = new LinkedList<ExpectedEvent>();
29
30 for (i = 0; i < nrTypes; i++)
31 this.events[i] = [];
32 }
33
39 public override async Task Load(bool isResuming, CancellationToken cancellationToken)
40 {
41 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>>? ByCategory = null;
42 List<NotificationEvent>? Events = null;
43 string? PrevCategory = null;
44 int PrevType = -1;
45 int Type;
46
47 IEnumerable<NotificationEvent> LoadedEvents;
48
49 try
50 {
51 LoadedEvents = await Database.Find<NotificationEvent>("Type", "Category");
52 }
53 catch (Exception ex)
54 {
55 ServiceRef.LogService.LogException(ex);
56
57 await Database.Clear("Notifications");
58 LoadedEvents = [];
59 }
60
61 foreach (NotificationEvent Event in LoadedEvents)
62 {
63 if (Event.Type is null || Event.Category is null)
64 continue;
65
66 Type = (int)Event.Type;
67 if (Type < 0 || Type >= nrTypes)
68 continue;
69
71 {
72 Log.Debug("Notification event of type " + Event.GetType().FullName + " lacked Category.");
73 await Database.Delete(Event);
74 continue;
75 }
76
77 lock (this.events)
78 {
79 if (ByCategory is null || Type != PrevType)
80 {
81 ByCategory = this.events[Type];
82 PrevType = Type;
83 }
84
85 if (Events is null || Event.Category != PrevCategory)
86 {
87 if (!ByCategory.TryGetValue(Event.Category, out Events))
88 {
89 Events = [];
90 ByCategory[Event.Category] = Events;
91 }
92
93 PrevCategory = Event.Category;
94 }
95
96 Events.Add(Event);
97 }
98 }
99
100 await base.Load(isResuming, cancellationToken);
101 }
102
108 public void ExpectEvent<T>(DateTime Before, Predicate<T> Predicate)
109 where T : NotificationEvent
110 {
111 // First, look for an existing event of type T matching the predicate.
112 NotificationEvent? MatchingEvent = null;
113 int NrFound = 0;
114
115 lock (this.events)
116 {
117 // Iterate over all types stored in the array.
118 // (You might also restrict the search if you know a priori the type’s index.)
119 for (int i = 0; i < nrTypes; i++)
120 {
121 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[i];
122 foreach (List<NotificationEvent> EventsList in ByCategory.Values)
123 {
124 foreach (NotificationEvent Evt in EventsList)
125 {
126 if (Evt is T TypedEvent && Predicate(TypedEvent))
127 {
128 MatchingEvent = Evt;
129 NrFound++;
130 break;
131 }
132 }
133 if (MatchingEvent is not null)
134 break;
135 }
136 if (MatchingEvent is not null)
137 break;
138 }
139 }
140
141 if (MatchingEvent is not null)
142 {
143 // Optionally remove the event from the in-memory collection
144 //RemoveEvent(matchingEvent);
145
146 // Run the event immediately on the main thread.
147 MainThread.BeginInvokeOnMainThread(async () =>
148 {
149 try
150 {
151 await MatchingEvent.Open();
152 }
153 catch (Exception ex)
154 {
155 ServiceRef.LogService.LogException(ex);
156 }
157 });
158 }
159 else
160 {
161 // Otherwise, add an expectation for a future event.
162 lock (this.expected)
163 {
164 this.expected.AddLast(new ExpectedEvent(
165 typeof(T),
166 Before,
167 (NotificationEvent e) => e is T t && Predicate(t)
168 ));
169 }
170 }
171 }
172
178 {
179 if (Event.Type is null || Event.Category is null)
180 return;
181
182 DateTime Now = DateTime.Now;
183 bool IsExpected = false;
184
185 lock (this.expected)
186 {
187 LinkedListNode<ExpectedEvent>? Node = this.expected.First;
188 while (Node is not null)
189 {
190 LinkedListNode<ExpectedEvent>? Next = Node.Next;
191 // Remove expired expectations.
192 if (Node.Value.Before < Now)
193 {
194 this.expected.Remove(Node);
195 }
196 else if (Node.Value.EventType == Event.GetType() &&
197 (Node.Value.Predicate is null || Node.Value.Predicate(Event)))
198 {
199 this.expected.Remove(Node);
200 IsExpected = true;
201 break;
202 }
203 Node = Next;
204 }
205 }
206
207 if (IsExpected)
208 {
209 MainThread.BeginInvokeOnMainThread(async () =>
210 {
211 try
212 {
213 await Event.Open();
214 }
215 catch (Exception Ex)
216 {
217 ServiceRef.LogService.LogException(Ex);
218 }
219 });
220 }
221 else
222 {
223 // Existing behavior: insert into database, add to events list, etc.
224 await Database.Insert(Event);
225 int Type = (int)Event.Type;
226 if (Type >= 0 && Type < nrTypes)
227 {
228 lock (this.events)
229 {
230 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[Type];
231 if (!ByCategory.TryGetValue(Event.Category, out List<NotificationEvent>? Events))
232 {
233 Events = new List<NotificationEvent>();
234 ByCategory[Event.Category] = Events;
235 }
236 Events.Add(Event);
237 }
238
239 await this.OnNewNotification.Raise(this, new NotificationEventArgs(Event));
240 }
241
242 // Prepare the event asynchronously.
243 Task _ = Task.Run(async () =>
244 {
245 try
246 {
247 await Event.Prepare();
248 }
249 catch (Exception Ex)
250 {
251 ServiceRef.LogService.LogException(Ex);
252 }
253 });
254 }
255 }
256
257
264 {
265 int TypeIndex = (int)Type;
266
267 if (TypeIndex >= 0 && TypeIndex < nrTypes)
268 {
269 NotificationEvent[] ToDelete;
270
271 lock (this.events)
272 {
273 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[TypeIndex];
274
275 if (!ByCategory.TryGetValue(Category, out List<NotificationEvent>? Events))
276 return;
277
278 ToDelete = [.. Events];
279 ByCategory.Remove(Category);
280 }
281
282 await this.DoDeleteEvents(ToDelete);
283 }
284 }
285
290 public Task DeleteEvents(params NotificationEvent[] Events)
291 {
292 foreach (NotificationEvent Event in Events)
293 {
294 if (Event.Type is null || Event.Category is null)
295 continue;
296
297 int TypeIndex = (int)Event.Type;
298
299 if (TypeIndex >= 0 && TypeIndex < nrTypes)
300 {
301 lock (this.events)
302 {
303 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[TypeIndex];
304
305 if (ByCategory.TryGetValue(Event.Category, out List<NotificationEvent>? List) &&
306 List.Remove(Event) &&
307 List.Count == 0)
308 {
309 ByCategory.Remove(Event.Category);
310 }
311 }
312 }
313
314 }
315
316 return this.DoDeleteEvents(Events);
317 }
318
319 private async Task DoDeleteEvents(NotificationEvent[] Events)
320 {
321 try
322 {
323 await Database.StartBulk();
324
325 try
326 {
327 foreach (NotificationEvent Event in Events)
328 {
329 try
330 {
331 await Database.Delete(Event);
332 }
333 catch (KeyNotFoundException)
334 {
335 // Ignore, already deleted.
336 }
337 }
338 }
339 finally
340 {
341 await Database.EndBulk();
342 }
343
344 await this.OnNotificationsDeleted.Raise(this, new NotificationEventsArgs(Events));
345 }
346 catch (Exception ex)
347 {
348 ServiceRef.LogService.LogException(ex);
349 }
350 }
351
358 {
359 int i = (int)Type;
360 if (i < 0 || i >= nrTypes)
361 return [];
362
363 lock (this.events)
364 {
365 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[i];
366 List<NotificationEvent> Result = [];
367
368 foreach (List<NotificationEvent> Events in ByCategory.Values)
369 Result.AddRange(Events);
370
371 return [.. Result];
372 }
373 }
374
381 {
382 int i = (int)Type;
383 if (i < 0 || i >= nrTypes)
384 return [];
385
386 lock (this.events)
387 {
388 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[i];
389 List<CaseInsensitiveString> Result = [.. ByCategory.Keys];
390
391 return [.. Result];
392 }
393 }
394
400 public SortedDictionary<CaseInsensitiveString, NotificationEvent[]> GetEventsByCategory(NotificationEventType Type)
401 {
402 int i = (int)Type;
403 if (i < 0 || i >= nrTypes)
404 return [];
405
406 lock (this.events)
407 {
408 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[i];
409 SortedDictionary<CaseInsensitiveString, NotificationEvent[]> Result = [];
410
411 foreach (KeyValuePair<CaseInsensitiveString, List<NotificationEvent>> P in ByCategory)
412 Result[P.Key] = [.. P.Value];
413
414 return Result;
415 }
416 }
417
423 public SortedDictionary<CaseInsensitiveString, T[]> GetEventsByCategory<T>(NotificationEventType Type)
424 where T : NotificationEvent
425 {
426 int i = (int)Type;
427 if (i < 0 || i >= nrTypes)
428 return [];
429
430 lock (this.events)
431 {
432 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[i];
433 SortedDictionary<CaseInsensitiveString, T[]> Result = [];
434
435 foreach (KeyValuePair<CaseInsensitiveString, List<NotificationEvent>> P in ByCategory)
436 {
437 List<T>? Items = null;
438
439 foreach (NotificationEvent Event in P.Value)
440 {
441 if (Event is T TypedItem)
442 {
443 Items ??= [];
444 Items.Add(TypedItem);
445 }
446 }
447
448 if (Items is not null)
449 Result[P.Key] = [.. Items];
450 }
451
452 return Result;
453 }
454 }
455
464 [NotNullWhen(true)] out NotificationEvent[]? Events)
465 {
466 int i = (int)Type;
467 if (i < 0 || i >= nrTypes)
468 {
469 Events = null;
470 return false;
471 }
472
473 lock (this.events)
474 {
475 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory = this.events[i];
476
477 if (!ByCategory.TryGetValue(Category, out List<NotificationEvent>? Events2))
478 {
479 Events = null;
480 return false;
481 }
482
483 Events = [.. Events2];
484 return true;
485 }
486 }
487
493 {
494 List<NotificationEvent> AllEvents = [];
495
496 lock (this.events)
497 {
498 // Loop through each event type.
499 foreach (SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory in this.events)
500 {
501 // Loop through each category list.
502 foreach (List<NotificationEvent> EventsList in ByCategory.Values)
503 {
504 AllEvents.AddRange(EventsList);
505 }
506 }
507 }
508
509 return [.. AllEvents];
510 }
511
512
516 public event EventHandlerAsync<NotificationEventArgs>? OnNewNotification;
517
521 public event EventHandlerAsync<NotificationEventsArgs>? OnNotificationsDeleted;
522
526 public int NrNotificationsContacts => this.Count((int)NotificationEventType.Contacts);
527
531 public int NrNotificationsThings => this.Count((int)NotificationEventType.Things);
532
536 public int NrNotificationsContracts => this.Count((int)NotificationEventType.Contracts);
537
541 public int NrNotificationsWallet => this.Count((int)NotificationEventType.Wallet);
542
543 private int Count(int Index)
544 {
545 lock (this.events)
546 {
547 SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> Events = this.events[Index];
548 int Result = 0;
549
550 foreach (List<NotificationEvent> List in Events.Values)
551 Result += List.Count;
552
553 return Result;
554 }
555 }
556
561 public async Task DeleteResolvedEvents(IEventResolver Resolver)
562 {
563 List<NotificationEvent>? Resolved = null;
564
565 lock (this.events)
566 {
567 foreach (SortedDictionary<CaseInsensitiveString, List<NotificationEvent>> ByCategory in this.events)
568 {
569 foreach (KeyValuePair<CaseInsensitiveString, List<NotificationEvent>> P in ByCategory)
570 {
571 foreach (NotificationEvent Event in P.Value)
572 {
573 if (Resolver.Resolves(Event))
574 {
575 Resolved ??= [];
576 Resolved.Add(Event);
577 }
578 }
579 }
580 }
581 }
582
583 if (Resolved is not null)
584 await this.DeleteEvents([.. Resolved]);
585 }
586
587 }
588}
SortedDictionary< CaseInsensitiveString, T[]> GetEventsByCategory< T >(NotificationEventType Type)
Gets available notification events for a button, sorted by category.
NotificationEvent[] GetEvents(NotificationEventType Type)
Gets available notification events for a button.
int NrNotificationsThings
Number of notifications but button Things
async Task NewEvent(NotificationEvent Event)
Registers a new event and notifies the user.
int NrNotificationsContracts
Number of notifications but button Contracts
void ExpectEvent< T >(DateTime Before, Predicate< T > Predicate)
Registers a type of notification as expected.
int NrNotificationsWallet
Number of notifications but button Wallet
EventHandlerAsync< NotificationEventsArgs >? OnNotificationsDeleted
Event raised when notifications have been deleted.
int NrNotificationsContacts
Number of notifications but button Contacts
override async Task Load(bool isResuming, CancellationToken cancellationToken)
Loads the specified service.
SortedDictionary< CaseInsensitiveString, NotificationEvent[]> GetEventsByCategory(NotificationEventType Type)
Gets available notification events for a button, sorted by category.
bool TryGetNotificationEvents(NotificationEventType Type, CaseInsensitiveString Category, [NotNullWhen(true)] out NotificationEvent[]? Events)
Tries to get available notification events.
CaseInsensitiveString[] GetCategories(NotificationEventType Type)
Gets available categories for a button.
EventHandlerAsync< NotificationEventArgs >? OnNewNotification
Event raised when a new notification has been logged.
async Task DeleteEvents(NotificationEventType Type, CaseInsensitiveString Category)
Deletes events for a given button and category.
Task DeleteEvents(params NotificationEvent[] Events)
Deletes a specified set of events.
NotificationEvent[] GetAllEvents()
Gets all notification events across all types and categories.
async Task DeleteResolvedEvents(IEventResolver Resolver)
Deletes pending events that have already been resolved.
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
Class representing an event.
Definition: Event.cs:11
EventType Type
Type of event.
Definition: Event.cs:122
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Debug(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs a debug event.
Definition: Log.cs:228
Represents a case-insensitive string.
static bool IsNullOrEmpty(CaseInsensitiveString value)
Indicates whether the specified string is null or an CaseInsensitiveString.Empty string.
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static Task EndBulk()
Ends bulk-processing of data. Must be called once for every call to StartBulk.
Definition: Database.cs:2259
static Task StartBulk()
Starts bulk-proccessing of data. Must be followed by a call to EndBulk.
Definition: Database.cs:2251
static async Task Delete(object Object)
Deletes an object in the database.
Definition: Database.cs:1291
static Task< IEnumerable< object > > Find(string Collection, params string[] SortOrder)
Finds objects in a given collection.
Definition: Database.cs:238
static async Task Insert(object Object)
Inserts an object into the default collection of the database.
Definition: Database.cs:97
static async Task Clear(string CollectionName)
Clears a collection of all objects.
Definition: Database.cs:1965
Interface for event resolvers. Such can be used to resolve multiple pending notifications at once.
bool Resolves(NotificationEvent Event)
If the resolver resolves an event.
Definition: ImplTypes.g.cs:58
abstract class NotificationEvent()
Abstract base class of notification events.
class NotificationEventsArgs(NotificationEvent[] Events)
Event argument for notification events.
class NotificationEventArgs(NotificationEvent Event)
Event argument for notification events.
NotificationEventType
Button on which event is to be displayed.