diff --git a/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs b/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs index 14cf03d..8e69c40 100644 --- a/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs +++ b/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs @@ -1,8 +1,15 @@ -using Shiny.Push; +using CommunityToolkit.Mvvm.Messaging; +using salesbook.Shared.Core.Dto.Notification; +using salesbook.Shared.Core.Interface.IntegryApi; +using salesbook.Shared.Core.Messages.Notification; +using Shiny.Push; namespace salesbook.Maui.Core.System.Notification.Push; -public class PushNotificationDelegate : IPushDelegate +public class PushNotificationDelegate( + IIntegryNotificationRestClient integryNotificationRestClient, + IMessenger messenger +) : IPushDelegate { public Task OnEntry(PushNotification notification) { @@ -12,14 +19,21 @@ public class PushNotificationDelegate : IPushDelegate public Task OnReceived(PushNotification notification) { - // fires when a push notification is received (silient or notification) - //notification.Data["content-available"] = "1"; + if (notification.Notification is null) return Task.CompletedTask; + var pushNotification = new PushNotificationDTO + { + Title = notification.Notification.Title, + Message = notification.Notification.Message + }; + + messenger.Send(new NewPushNotificationMessage(pushNotification)); + return Task.CompletedTask; } public Task OnNewToken(string token) { - // fires when a push notification change is set by the operating system or provider + integryNotificationRestClient.Register(token); return Task.CompletedTask; } diff --git a/salesbook.Maui/MauiProgram.cs b/salesbook.Maui/MauiProgram.cs index 1743cbf..c9c7290 100644 --- a/salesbook.Maui/MauiProgram.cs +++ b/salesbook.Maui/MauiProgram.cs @@ -22,6 +22,7 @@ using salesbook.Shared.Core.Messages.Activity.Copy; using salesbook.Shared.Core.Messages.Activity.New; using salesbook.Shared.Core.Messages.Back; using salesbook.Shared.Core.Messages.Contact; +using salesbook.Shared.Core.Messages.Notification; using salesbook.Shared.Core.Services; using Shiny; @@ -71,14 +72,16 @@ namespace salesbook.Maui builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); //Message - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); //Notification builder.Services.AddNotifications(); diff --git a/salesbook.Maui/wwwroot/index.html b/salesbook.Maui/wwwroot/index.html index 74faadd..7f7b15f 100644 --- a/salesbook.Maui/wwwroot/index.html +++ b/salesbook.Maui/wwwroot/index.html @@ -53,6 +53,7 @@ + diff --git a/salesbook.Shared/Components/Layout/NavMenu.razor b/salesbook.Shared/Components/Layout/NavMenu.razor index a4cdf53..6880a7d 100644 --- a/salesbook.Shared/Components/Layout/NavMenu.razor +++ b/salesbook.Shared/Components/Layout/NavMenu.razor @@ -1,13 +1,18 @@ @using CommunityToolkit.Mvvm.Messaging @using salesbook.Shared.Core.Dto @using salesbook.Shared.Core.Dto.Activity +@using salesbook.Shared.Core.Dto.Notification +@using salesbook.Shared.Core.Dto.PageState @using salesbook.Shared.Core.Entity @using salesbook.Shared.Core.Messages.Activity.Copy @using salesbook.Shared.Core.Messages.Activity.New @using salesbook.Shared.Core.Messages.Contact +@using salesbook.Shared.Core.Messages.Notification @inject IDialogService Dialog @inject IMessenger Messenger @inject CopyActivityService CopyActivityService +@inject NewPushNotificationService NewPushNotificationService +@inject NotificationState Notification
@@ -63,8 +70,9 @@ protected override Task OnInitializedAsync() { CopyActivityService.OnCopyActivity += async dto => await CreateActivity(dto); + NewPushNotificationService.OnNotificationReceived += NewNotificationReceived; - NavigationManager.LocationChanged += (_, args) => + NavigationManager.LocationChanged += (_, args) => { var location = args.Location.Remove(0, NavigationManager.BaseUri.Length); @@ -104,4 +112,10 @@ Messenger.Send(new NewContactMessage((CRMCreateContactResponseDTO)result.Data)); } } + + private void NewNotificationReceived(PushNotificationDTO notification) + { + Notification.UnreadNotifications.Add(notification); + InvokeAsync(StateHasChanged); + } } \ No newline at end of file diff --git a/salesbook.Shared/Components/Pages/Notifications.razor b/salesbook.Shared/Components/Pages/Notifications.razor index 9579556..494a3d4 100644 --- a/salesbook.Shared/Components/Pages/Notifications.razor +++ b/salesbook.Shared/Components/Pages/Notifications.razor @@ -2,13 +2,45 @@ @attribute [Authorize] @using salesbook.Shared.Components.Layout @using salesbook.Shared.Components.SingleElements +@using salesbook.Shared.Core.Dto.Notification +@using salesbook.Shared.Core.Dto.PageState +@using salesbook.Shared.Core.Messages.Notification +@inject NotificationState Notification +@inject NewPushNotificationService NewPushNotificationService +@inject IJSRuntime JS
- + @if (Notification.UnreadNotifications.IsNullOrEmpty()) + { + + } + else + { +
+ @foreach(var notification in Notification.UnreadNotifications) + { + + } +
+ }
@code { + protected override Task OnInitializedAsync() + { + NewPushNotificationService.OnNotificationReceived += NewNotificationReceived; + return Task.CompletedTask; + } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JS.InvokeVoidAsync("initNotifications"); + } + + private void NewNotificationReceived(PushNotificationDTO notification) + { + InvokeAsync(StateHasChanged); + } } \ No newline at end of file diff --git a/salesbook.Shared/Components/Pages/Notifications.razor.css b/salesbook.Shared/Components/Pages/Notifications.razor.css index e69de29..6f08dcd 100644 --- a/salesbook.Shared/Components/Pages/Notifications.razor.css +++ b/salesbook.Shared/Components/Pages/Notifications.razor.css @@ -0,0 +1,6 @@ +.list { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} diff --git a/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor new file mode 100644 index 0000000..873e9f7 --- /dev/null +++ b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor @@ -0,0 +1,15 @@ +@using salesbook.Shared.Core.Dto.Notification + +
+
+
+
+
@Notification.Title
+
@Notification.Message
+
+
+
+ +@code { + [Parameter] public PushNotificationDTO Notification { get; set; } = new(); +} \ No newline at end of file diff --git a/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor.css b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor.css new file mode 100644 index 0000000..2c63bb0 --- /dev/null +++ b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor.css @@ -0,0 +1,64 @@ +.row { + position: relative; + overflow: hidden; + border-radius: var(--mud-default-borderradius); + box-shadow: var(--custom-box-shadow); + width: 100%; +} + +.behind { + position: absolute; + inset: 0; + display: flex; + justify-content: flex-end; + align-items: center; + padding-right: 14px; + background: var(--mud-palette-error); + z-index: 0; +} + +.trash-btn { + color: white; + padding: 10px 15px; + cursor: pointer; +} + +.notification-card { + position: relative; + z-index: 1; + display: flex; + align-items: center; + padding: 12px; + background: var(--mud-palette-background); + transition: transform .2s ease; + touch-action: pan-y; + will-change: transform; + transform: translateX(0); +} + +.avatar { + width: 42px; + height: 42px; + border-radius: 12px; + display: grid; + place-items: center; + background: #0b1220; + border: 1px solid #1f2937; + font-weight: bold; + color: #22d3ee; +} + +.title { + font-weight: 700; + margin-bottom: unset !important; +} + +.subtitle { + font-size: 13px; + color: #94a3b8; +} + +.collapsing { + transition: height .22s ease, margin .22s ease, opacity .22s ease; + overflow: hidden; +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Dto/Notification/PushNotificationDTO.cs b/salesbook.Shared/Core/Dto/Notification/PushNotificationDTO.cs new file mode 100644 index 0000000..91de6df --- /dev/null +++ b/salesbook.Shared/Core/Dto/Notification/PushNotificationDTO.cs @@ -0,0 +1,9 @@ +namespace salesbook.Shared.Core.Dto.Notification; + +public class PushNotificationDTO +{ + public string? Title { get; set; } + public string? Message { get; set; } + + public DateTime Hours => DateTime.Now; +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Dto/PageState/NotificationState.cs b/salesbook.Shared/Core/Dto/PageState/NotificationState.cs new file mode 100644 index 0000000..cfaf940 --- /dev/null +++ b/salesbook.Shared/Core/Dto/PageState/NotificationState.cs @@ -0,0 +1,8 @@ +using salesbook.Shared.Core.Dto.Notification; + +namespace salesbook.Shared.Core.Dto.PageState; + +public class NotificationState +{ + public List UnreadNotifications { get; set; } = []; +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Messages/Notification/NewPushNotificationMessage.cs b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationMessage.cs new file mode 100644 index 0000000..ff48cec --- /dev/null +++ b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationMessage.cs @@ -0,0 +1,6 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; +using salesbook.Shared.Core.Dto.Notification; + +namespace salesbook.Shared.Core.Messages.Notification; + +public class NewPushNotificationMessage(PushNotificationDTO value) : ValueChangedMessage(value); \ No newline at end of file diff --git a/salesbook.Shared/Core/Messages/Notification/NewPushNotificationService.cs b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationService.cs new file mode 100644 index 0000000..e863668 --- /dev/null +++ b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationService.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Mvvm.Messaging; +using salesbook.Shared.Core.Dto.Notification; + +namespace salesbook.Shared.Core.Messages.Notification; + +public class NewPushNotificationService +{ + public event Action? OnNotificationReceived; + + public NewPushNotificationService(IMessenger messenger) + { + messenger.Register(this, (_, o) => { OnNotificationReceived?.Invoke(o.Value); }); + } +} \ No newline at end of file diff --git a/salesbook.Shared/wwwroot/js/notifications.js b/salesbook.Shared/wwwroot/js/notifications.js new file mode 100644 index 0000000..f38633c --- /dev/null +++ b/salesbook.Shared/wwwroot/js/notifications.js @@ -0,0 +1,85 @@ +const FIRST_THRESHOLD = 80; // rivela pulsante +const SECOND_THRESHOLD = 160; // elimina diretta +const CLOSE_THRESHOLD = 40; // swipe a destra per richiudere +const MAX_SWIPE = 200; + +window.initNotifications = () => { + document.querySelectorAll('.row').forEach(initRow); +}; + +function initRow(row) { + const card = row.querySelector('.notification-card'); + const btn = row.querySelector('.trash-btn'); + let startX = 0, currentX = 0, dragging = false; + let open = false; + + card.addEventListener('pointerdown', (e) => { + if (e.pointerType === 'mouse' && e.button !== 0) return; + dragging = true; + startX = e.clientX; + card.setPointerCapture(e.pointerId); + card.style.transition = 'none'; + }); + + card.addEventListener('pointermove', (e) => { + if (!dragging) return; + const dx = e.clientX - startX; + let translate = dx; + + if (!open) { + // swiping left to open/delete + translate = Math.max(-MAX_SWIPE, Math.min(0, dx)); + } else { + // if already open, allow swipe right to close + translate = Math.min(0, -FIRST_THRESHOLD + dx); + } + currentX = translate; + card.style.transform = `translateX(${translate}px)`; + }); + + function endDrag() { + if (!dragging) return; + dragging = false; + card.style.transition = 'transform .2s ease'; + + if (!open) { + if (currentX < -SECOND_THRESHOLD) { + card.style.transform = `translateX(-${MAX_SWIPE}px)`; + setTimeout(() => removeRow(row), 200); + } else if (currentX < -FIRST_THRESHOLD) { + card.style.transform = `translateX(-${FIRST_THRESHOLD}px)`; + open = true; + } else { + card.style.transform = 'translateX(0)'; + open = false; + } + } else { + // se è già aperto e vado a destra abbastanza → chiudo + if (currentX > -FIRST_THRESHOLD + CLOSE_THRESHOLD) { + card.style.transform = 'translateX(0)'; + open = false; + } else { + card.style.transform = `translateX(-${FIRST_THRESHOLD}px)`; + open = true; + } + } + } + + card.addEventListener('pointerup', endDrag); + card.addEventListener('pointercancel', endDrag); + + btn.addEventListener('click', () => removeRow(row)); +} + +function removeRow(row) { + const h = row.getBoundingClientRect().height; + row.style.height = h + 'px'; + row.classList.add('collapsing'); + requestAnimationFrame(() => { + row.style.opacity = '0'; + row.style.marginTop = '0'; + row.style.marginBottom = '0'; + row.style.height = '0'; + }); + setTimeout(() => row.remove(), 220); +} \ No newline at end of file