From 73362f3981d3a8063818c8d6e8a011d10a79b8c0 Mon Sep 17 00:00:00 2001 From: abdo samara Date: Fri, 5 Aug 2022 14:25:11 +0200 Subject: [PATCH 1/3] Squashed commit of the following: commit 8327716d1d39753b397f237edfefae6c9c55f1ec Author: elmerbool <35117496+elmerbool@users.noreply.github.com> Date: Thu Aug 4 22:26:55 2022 +1000 Updated nucleus client and image version. commit f5cadf496dc40966ddf817068db154a87cb378d2 Author: Junvic Date: Tue Aug 2 19:32:04 2022 +0800 #19: integrate push notifications API calls (#55) --- src/docker-compose.yml | 2 +- src/main/Application/Application.csproj | 2 +- .../ISubscriptionApplicationService.cs | 10 ++ .../ISubscriptionQueryService.cs | 11 ++ .../SubscriptionApplicationService.cs | 33 +++++ .../Subscriptions/SubscriptionQueryService.cs | 28 +++++ src/main/Port.Adapter/Common/Common.csproj | 1 - .../IO/Process/Services/Services.csproj | 1 - .../UI/Views/Blazor/Blazor.csproj | 17 +++ .../UI/Views/Blazor/Pages/Tree.razor | 102 +++++++++++++--- .../Port.Adapter/UI/Views/Blazor/Startup.cs | 6 + .../UI/Views/Blazor/wwwroot/JsInterop.js | 113 ++++++++++++++++++ .../Views/Blazor/wwwroot/notifications-sw.js | 28 +++++ 13 files changed, 331 insertions(+), 23 deletions(-) create mode 100644 src/main/Application/Subscriptions/ISubscriptionApplicationService.cs create mode 100644 src/main/Application/Subscriptions/ISubscriptionQueryService.cs create mode 100644 src/main/Application/Subscriptions/SubscriptionApplicationService.cs create mode 100644 src/main/Application/Subscriptions/SubscriptionQueryService.cs create mode 100644 src/main/Port.Adapter/UI/Views/Blazor/wwwroot/notifications-sw.js diff --git a/src/docker-compose.yml b/src/docker-compose.yml index f7dd782..4386db7 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.4' services: blazor: - image: ei8works/d23.api:0.2.3 + image: ei8works/d23.api:0.2.4 build: context: . dockerfile: ./main/Port.Adapter/UI/Views/Blazor/Dockerfile diff --git a/src/main/Application/Application.csproj b/src/main/Application/Application.csproj index 68c72a6..087f967 100644 --- a/src/main/Application/Application.csproj +++ b/src/main/Application/Application.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/main/Application/Subscriptions/ISubscriptionApplicationService.cs b/src/main/Application/Subscriptions/ISubscriptionApplicationService.cs new file mode 100644 index 0000000..2ea8e15 --- /dev/null +++ b/src/main/Application/Subscriptions/ISubscriptionApplicationService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace ei8.Cortex.Diary.Application.Subscriptions +{ + public interface ISubscriptionApplicationService + { + Task SubscribeAsync(string avatarUrl, string avatarSnapshotUrl, string deviceName, string pushAuth, string pushP256dh, string pushEndpoint); + } +} diff --git a/src/main/Application/Subscriptions/ISubscriptionQueryService.cs b/src/main/Application/Subscriptions/ISubscriptionQueryService.cs new file mode 100644 index 0000000..ec983cf --- /dev/null +++ b/src/main/Application/Subscriptions/ISubscriptionQueryService.cs @@ -0,0 +1,11 @@ +using ei8.Cortex.Subscriptions.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace ei8.Cortex.Diary.Application.Subscriptions +{ + public interface ISubscriptionQueryService + { + Task GetServerConfigurationAsync(string avatarUrl, CancellationToken token = default); + } +} diff --git a/src/main/Application/Subscriptions/SubscriptionApplicationService.cs b/src/main/Application/Subscriptions/SubscriptionApplicationService.cs new file mode 100644 index 0000000..7133a5a --- /dev/null +++ b/src/main/Application/Subscriptions/SubscriptionApplicationService.cs @@ -0,0 +1,33 @@ +using ei8.Cortex.Diary.Domain.Model; +using ei8.Cortex.Diary.Nucleus.Client.In; +using ei8.Cortex.Subscriptions.Common; +using System.Threading.Tasks; + +namespace ei8.Cortex.Diary.Application.Subscriptions +{ + public class SubscriptionApplicationService : ISubscriptionApplicationService + { + private readonly ISubscriptionClient subscriptionClient; + private readonly ITokenManager tokenManager; + + public SubscriptionApplicationService(ISubscriptionClient subscriptionClient, ITokenManager tokenManager) + { + this.subscriptionClient = subscriptionClient; + this.tokenManager = tokenManager; + } + + public async Task SubscribeAsync(string avatarUrl, string avatarSnapshotUrl, string deviceName, string pushAuth, string pushP256dh, string pushEndpoint) + { + var token = await this.tokenManager.RetrieveAccessTokenAsync(); + + await this.subscriptionClient.AddSubscriptionAsync(avatarUrl, new BrowserSubscriptionInfo() + { + AvatarUrl = avatarUrl, + Name = deviceName, + PushAuth = pushAuth, + PushEndpoint = pushEndpoint, + PushP256DH = pushP256dh + }, token); + } + } +} diff --git a/src/main/Application/Subscriptions/SubscriptionQueryService.cs b/src/main/Application/Subscriptions/SubscriptionQueryService.cs new file mode 100644 index 0000000..88da201 --- /dev/null +++ b/src/main/Application/Subscriptions/SubscriptionQueryService.cs @@ -0,0 +1,28 @@ +using ei8.Cortex.Diary.Application.Settings; +using ei8.Cortex.Diary.Nucleus.Client.Out; +using ei8.Cortex.Subscriptions.Common; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ei8.Cortex.Diary.Application.Subscriptions +{ + public class SubscriptionQueryService : ISubscriptionQueryService + { + private readonly ISubscriptionConfigurationClient client; + private readonly ISettingsService settings; + + public SubscriptionQueryService(ISubscriptionConfigurationClient client, ISettingsService settings) + { + this.client = client; + this.settings = settings; + } + + public async Task GetServerConfigurationAsync(string avatarUrl, CancellationToken token = default) + { + return await this.client.GetServerConfigurationAsync(avatarUrl); + } + } +} diff --git a/src/main/Port.Adapter/Common/Common.csproj b/src/main/Port.Adapter/Common/Common.csproj index 2d77ca8..be41f7e 100644 --- a/src/main/Port.Adapter/Common/Common.csproj +++ b/src/main/Port.Adapter/Common/Common.csproj @@ -8,7 +8,6 @@ - diff --git a/src/main/Port.Adapter/IO/Process/Services/Services.csproj b/src/main/Port.Adapter/IO/Process/Services/Services.csproj index 2d7cd09..1554758 100644 --- a/src/main/Port.Adapter/IO/Process/Services/Services.csproj +++ b/src/main/Port.Adapter/IO/Process/Services/Services.csproj @@ -12,7 +12,6 @@ - diff --git a/src/main/Port.Adapter/UI/Views/Blazor/Blazor.csproj b/src/main/Port.Adapter/UI/Views/Blazor/Blazor.csproj index fdbc437..adfef26 100644 --- a/src/main/Port.Adapter/UI/Views/Blazor/Blazor.csproj +++ b/src/main/Port.Adapter/UI/Views/Blazor/Blazor.csproj @@ -23,6 +23,7 @@ + @@ -40,8 +41,24 @@ Always + + Always + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + \ No newline at end of file diff --git a/src/main/Port.Adapter/UI/Views/Blazor/Pages/Tree.razor b/src/main/Port.Adapter/UI/Views/Blazor/Pages/Tree.razor index f2c41d7..a3d9bd6 100644 --- a/src/main/Port.Adapter/UI/Views/Blazor/Pages/Tree.razor +++ b/src/main/Port.Adapter/UI/Views/Blazor/Pages/Tree.razor @@ -1,5 +1,6 @@ @page "/tree" @implements IDisposable +@using ei8.Cortex.Diary.Application.Subscriptions @using ei8.Cortex.Diary.Domain.Model @using ei8.Cortex.Diary.Port.Adapter.Common @using ei8.Cortex.Diary.Port.Adapter.UI.Views.Blazor.ViewModels @@ -32,6 +33,8 @@ @inject IJSRuntime jsRuntime @inject ISettingsService settingsService @inject IIdentityService identityService +@inject ISubscriptionApplicationService subscriptionApplicationService +@inject ISubscriptionQueryService subscriptionsQueryService @@ -60,6 +63,10 @@ Copy Direct Avatar URL + + + Subscribe + @if (this.RenderDirection == RenderDirectionValue.TopToBottom) @@ -237,13 +244,17 @@ else if (!urlSet) await this.SetReloading(false); - this.refreshTimer = new Timer(); - this.refreshTimer.Interval = this.settingsService.UpdateCheckInterval; - this.refreshTimer.Elapsed += OnTimerInterval; - this.refreshTimer.AutoReset = true; - // Start the timer - this.refreshTimer.Enabled = true; - } + this.refreshTimer = new Timer(); + this.refreshTimer.Interval = this.settingsService.UpdateCheckInterval; + this.refreshTimer.Elapsed += OnTimerInterval; + this.refreshTimer.AutoReset = true; + // Start the timer + this.refreshTimer.Enabled = true; + + // register push notification service worker + var objRef = DotNetObjectReference.Create(this); + await this.jsRuntime.InvokeVoidAsync("RegisterServiceWorker", objRef); + } await base.OnAfterRenderAsync(firstRender); } @@ -395,17 +406,23 @@ else ((List)this.Children).AddRange(children); this.NewItemsCount = 0; - if (this.RenderDirection == RenderDirectionValue.BottomToTop) - await this.ScrollToFragment("bottom"); - } - catch (Exception ex) - { - string description, clipboardData; - if (!Blazor.Helper.TryGetHttpRequestExceptionExDetalis(ex, out description, out clipboardData)) - { - description = ex.Message; - clipboardData = ex.ToString(); - } + if (this.RenderDirection == RenderDirectionValue.BottomToTop) + await this.ScrollToFragment("bottom"); + + QueryUrl.TryParse(this.AvatarUrl, out QueryUrl result); + this.serverPushPublicKey = (await this.subscriptionsQueryService.GetServerConfigurationAsync(result.AvatarUrl)).ServerPublicKey; + + var objRef = DotNetObjectReference.Create(this); + await this.jsRuntime.InvokeVoidAsync("Subscribe", objRef, this.serverPushPublicKey); + } + catch (Exception ex) + { + string description, clipboardData; + if (!Blazor.Helper.TryGetHttpRequestExceptionExDetalis(ex, out description, out clipboardData)) + { + description = ex.Message; + clipboardData = ex.ToString(); + } Blazor.Helper.ShowFriendlyException( this.toastService, @@ -522,7 +539,7 @@ else children.ToList().ForEach(c => Tree.ExtractLinks(c.Children.ToArray(), distinctNodes, links)); } - private void CopyAvatarUrl() + private void CopyAvatarUrl() { jsRuntime.InvokeVoidAsync("copyToClipboard", Tree.BuildAvatarUrl(this.navigationManager.Uri, this.AvatarUrl)); this.toastService.ShowInfo($"Copied successfully."); @@ -536,4 +553,51 @@ else var encodedUrl = System.Net.WebUtility.UrlEncode(avatarUrl); return parsedBasedUrl + prefixKeyword + encodedUrl; } + + private async Task Subscribe() + { + QueryUrl.TryParse(this.AvatarUrl, out QueryUrl result); + await this.subscriptionApplicationService.SubscribeAsync(result.AvatarUrl, this.AvatarUrl, this.deviceName, this.pushAuth, this.pushP256DH, this.pushEndpoint); + } + + #region Subscription Interop + private bool notificationsSupported = false; + private bool serviceWorkerRegistered = false; + private string permissionStatus = string.Empty; + private string deviceName = string.Empty; + private string pushP256DH; + private string pushAuth; + private string pushEndpoint; + private string serverPushPublicKey; + + [JSInvokable] + public void SetPermissionStatus(string status) + { + permissionStatus = status; + StateHasChanged(); + } + + [JSInvokable] + public void SetSupportState(bool isNotificationSupported) + { + notificationsSupported = isNotificationSupported; + StateHasChanged(); + } + + [JSInvokable] + public void SetServiceWorkerStatus(bool isRegistered) + { + serviceWorkerRegistered = isRegistered; + StateHasChanged(); + } + + [JSInvokable] + public void SetDeviceProperties(string name, string pushp256dh, string pushAuth, string pushEndpoint) + { + this.deviceName = name; + this.pushP256DH = pushp256dh; + this.pushAuth = pushAuth; + this.pushEndpoint = pushEndpoint; + } + #endregion } \ No newline at end of file diff --git a/src/main/Port.Adapter/UI/Views/Blazor/Startup.cs b/src/main/Port.Adapter/UI/Views/Blazor/Startup.cs index 7c7fb6d..8fec1a4 100644 --- a/src/main/Port.Adapter/UI/Views/Blazor/Startup.cs +++ b/src/main/Port.Adapter/UI/Views/Blazor/Startup.cs @@ -8,6 +8,7 @@ using ei8.Cortex.Diary.Application.Neurons; using ei8.Cortex.Diary.Application.Notifications; using ei8.Cortex.Diary.Application.Settings; +using ei8.Cortex.Diary.Application.Subscriptions; using ei8.Cortex.Diary.Domain.Model; using ei8.Cortex.Diary.Nucleus.Client.In; using ei8.Cortex.Diary.Nucleus.Client.Out; @@ -104,6 +105,11 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + var vas = new ViewApplicationService(new ViewRepository()); services.AddSingleton>(vas.GetAll().Result); services.AddAuthentication(options => diff --git a/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/JsInterop.js b/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/JsInterop.js index cb76e6f..0beda44 100644 --- a/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/JsInterop.js +++ b/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/JsInterop.js @@ -6,4 +6,117 @@ window.PlaySound = function () { document.getElementById('sound').play(); +} + +// -- Subscriptions JS interop +window.RegisterServiceWorker = function(dotnet) { + if (!('PushManager' in window)) { + console.log('Push notifications are not supported in this browser.'); + return; + } + + Notification.requestPermission().then(function (status) { + console.log('Status: ' + status); + dotnet.invokeMethodAsync('SetPermissionStatus', status); + + if (status === 'denied') { + console.log('User denied notification permissions'); + return; + } + else if (status === 'granted') { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('./notifications-sw.js').then(function (registration) { + if (registration.installing) { + console.log('Service worker installing'); + } else if (registration.waiting) { + console.log('Service worker installed'); + } else if (registration.active) { + console.log('Service worker active'); + } + }); + } else { + console.log('Browser does not support service workers. Push notifications may not work.'); + } + } + }); +} + +window.Subscribe = function (dotnet, applicationServerPublicKey) { + if (navigator.serviceWorker) { + navigator.serviceWorker.ready.then(function (reg) { + + if (reg.active) { + dotnet.invokeMethodAsync('SetServiceWorkerStatus', true); + + const subscribeParams = { userVisibleOnly: true }; + + //Setting the public key of our VAPID key pair. + const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey); + subscribeParams.applicationServerKey = applicationServerKey; + + if (!(reg.showNotification)) { + console.log('Browser does not support off-site push notifications.'); + } else { + dotnet.invokeMethodAsync('SetSupportState', true); + + reg.pushManager.subscribe(subscribeParams) + .then(function (subscription) { + const p256dh = base64Encode(subscription.getKey('p256dh')); + const auth = base64Encode(subscription.getKey('auth')); + + console.log(subscription); + + console.log(subscription.endpoint); + console.log(p256dh); + console.log(auth); + + dotnet.invokeMethodAsync('SetDeviceProperties', detectBrowser(), p256dh, auth, subscription.endpoint); + }) + .catch(function (e) { + console.log('[subscribe] Unable to subscribe to push', e); + }); + } + } + }); + } +} + +function urlB64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + +function base64Encode(arrayBuffer) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))); +} + +function detectBrowser() { + const agent = navigator.userAgent + if ((agent.indexOf("Opera") || agent.indexOf('OPR')) != -1) { + return 'Opera'; + } else if (agent.indexOf("Edge") != -1) { + return 'MS Edge'; + } else if (agent.indexOf("Edg") != -1) { + return 'Chromium Edge'; + } else if (agent.indexOf("Chrome") != -1) { + return 'Chrome'; + } else if (agent.indexOf("Safari") != -1) { + return 'Safari'; + } else if (agent.indexOf("Firefox") != -1) { + return 'Firefox'; + } else if ((agent.indexOf("MSIE") != -1) || (!!document.documentMode == true)) { + return 'IE'; + } else { + return 'Unknown'; + } } \ No newline at end of file diff --git a/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/notifications-sw.js b/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/notifications-sw.js new file mode 100644 index 0000000..2659fed --- /dev/null +++ b/src/main/Port.Adapter/UI/Views/Blazor/wwwroot/notifications-sw.js @@ -0,0 +1,28 @@ +self.addEventListener('push', function (event) { + if (!(self.Notification && self.Notification.permission === 'granted')) { + return; + } + + let data = {}; + + if (event.data) { + data = event.data.json(); + } + + event.waitUntil(self.registration.showNotification(data.title, data.options)); +}); + +self.addEventListener('notificationclick', function (event) { + event.notification.close(); + + const tag = event.notification.tag; + const targetUrl = `/landing/${tag}`; + + event.waitUntil(clients.matchAll({ + type: "window" + }).then(function (clientList) { + // open a new window/tab to the target URL + if (clients.openWindow) + return clients.openWindow(targetUrl); + })); +}); \ No newline at end of file From 281872b995ba4d73fe7c83e5bd2bf14cdd820f1c Mon Sep 17 00:00:00 2001 From: Abdelrhman Ahmed <64210458+AbdulrahmanAhmeed@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:49:46 +0200 Subject: [PATCH 2/3] Comment_GetUrlType_Method --- .../UI/Views/Blazor.Common/TreeView.razor | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor b/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor index 2a11d6d..4f6a316 100644 --- a/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor +++ b/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor @@ -267,23 +267,23 @@ { UrlType result = UrlType.Invalid; - if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) - { - result = UrlType.Unrecognized; + // if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) + // { + // result = UrlType.Unrecognized; - if (url.StartsWith("https://drive.google.com/file") && url.EndsWith("/preview")) - { - result = UrlType.GoogleDriveVideo; - } - else - { - using var client = new HttpClient(); - var response = client.Send(new HttpRequestMessage(HttpMethod.Head, url)); - if (response.Content.Headers.ContentType.MediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) - result = UrlType.GoogleDriveImage; - } - } + // if (url.StartsWith("https://drive.google.com/file") && url.EndsWith("/preview")) + // { + // result = UrlType.GoogleDriveVideo; + // } + // else + // { + // using var client = new HttpClient(); + // var response = client.Send(new HttpRequestMessage(HttpMethod.Head, url)); + // if (response.Content.Headers.ContentType.MediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + // result = UrlType.GoogleDriveImage; + // } + // } return result; } From 8ebe2480efac914301c38bb84ae610f1943ffb69 Mon Sep 17 00:00:00 2001 From: Abdelrhman Ahmed <64210458+AbdulrahmanAhmeed@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:33:49 +0200 Subject: [PATCH 3/3] Add_TODO_Comment --- src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor b/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor index 4f6a316..22d3289 100644 --- a/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor +++ b/src/main/Port.Adapter/UI/Views/Blazor.Common/TreeView.razor @@ -267,6 +267,7 @@ { UrlType result = UrlType.Invalid; + //TODO // if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) // { // result = UrlType.Unrecognized; @@ -284,6 +285,7 @@ // result = UrlType.GoogleDriveImage; // } // } + return result; }