From de88f76f0be9531c0876bf879087aff5612c6a0d Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 28 Jul 2024 16:24:16 +0530 Subject: [PATCH 01/11] initial config --- frontend/public/manifest.json | 4 +- frontend/public/service-worker.js | 53 +++++++++++++ .../notifications/SlideOverNotifications.jsx | 53 +++++++++++-- frontend/src/index.js | 3 + frontend/src/serviceWorkerRegistration.js | 28 +++++++ .../entity/NotificationSubscription.java | 78 +++++++++++++++++++ src/main/resources/application.properties | 6 ++ .../2.8.x.x/notificationsubsriptions.xml | 29 +++++++ 8 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 frontend/public/service-worker.js create mode 100644 frontend/src/serviceWorkerRegistration.js create mode 100644 src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java create mode 100644 src/main/resources/liquibase/2.8.x.x/notificationsubsriptions.xml diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 302a19a512..d39c65ff48 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "OpenELIS", + "name": "OpenELIS Global", "icons": [ { "src": "images/favicon-16x16.png", diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js new file mode 100644 index 0000000000..1524edc672 --- /dev/null +++ b/frontend/public/service-worker.js @@ -0,0 +1,53 @@ +// Any other custom service worker logic can go here. + +self.addEventListener("push", async function (event) { + if (event.data) { + const data = event.data.json(); + const notificationOptions = { + body: data.body || "Sample Body", + tag: data.external_id || "default-tag", + }; + + event.waitUntil( + self.registration.showNotification("OpenELIS Test Message Received", notificationOptions) + ); + } +}); + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } +}); + + +// self.addEventListener('push', (e) => { +// const data = e.data?.json(); +// if (data) { +// self.registration.showNotification(data.title, { +// body: data.body, +// }); +// } +// }); + +// self.addEventListener('notificationclick', (e) => { +// e.notification.close(); +// e.waitUntil(focusOrOpenWindow()); +// }); + +// async function focusOrOpenWindow() { +// const url = new URL('/', self.location.origin).href; + +// const allWindows = await self.clients.matchAll({ +// type: 'window', +// }); +// const appWindow = allWindows.find((w) => w.url === url); + +// if (appWindow) { +// return appWindow.focus(); +// } else { +// return self.clients.openWindow(url); +// } +// } \ No newline at end of file diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index b2ed3a4289..fccd9a78ad 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -3,9 +3,50 @@ import { formatTimestamp } from "../utils/Utils"; import Spinner from "../common/Sprinner"; import { FormattedMessage, useIntl } from "react-intl"; + + + + export default function SlideOverNotifications(props) { const intl = useIntl(); + async function subscribe() { + + + const public_key = "BJDIyXHWK_o9fYNwD3fUie2Ed04-yx5fxz9-GUT1c0QhfdDiGMvVbJwvB_On3XapXqIRR471uh7Snw3bfPt9niw"; + const sw = await navigator.serviceWorker.ready; + const push = await sw.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: public_key, + }); + const p256dh = btoa( + String.fromCharCode.apply( + null, + new Uint8Array(push.getKey("p256dh")), + ), + ); + const auth = btoa( + String.fromCharCode.apply( + null, + new Uint8Array(push.getKey("auth")), + ), + ); + + const data = { + pf_endpoint: push.endpoint, + pf_p256dh: p256dh, + pf_auth: auth, + }; + + console.log("subsription details", data) + + + + + + } + + const { loading, notifications, @@ -80,13 +121,13 @@ export default function SlideOverNotifications(props) { label: intl.formatMessage({ id: "notification.slideover.button.subscribe", }), - onClick: () => {}, + onClick: () => subscribe(), }, { icon: , label: intl.formatMessage({ id: "notification.slideover.button.markallasread", - }), + }), onClick: () => { markAllNotificationsAsRead(); }, @@ -95,11 +136,11 @@ export default function SlideOverNotifications(props) { icon: , label: showRead ? intl.formatMessage({ - id: "notification.slideover.button.hideread", - }) + id: "notification.slideover.button.hideread", + }) : intl.formatMessage({ - id: "notification.slideover.button.showread", - }), + id: "notification.slideover.button.showread", + }), onClick: () => setShowRead(!showRead), }, ].map(({ icon, label, onClick }, index) => ( diff --git a/frontend/src/index.js b/frontend/src/index.js index 211e34a603..2027da061c 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,6 +3,9 @@ import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; +import * as ServiceWorker from "./serviceWorkerRegistration" + +ServiceWorker.registerServiceWorker() ReactDOM.render( diff --git a/frontend/src/serviceWorkerRegistration.js b/frontend/src/serviceWorkerRegistration.js new file mode 100644 index 0000000000..70ce591f3f --- /dev/null +++ b/frontend/src/serviceWorkerRegistration.js @@ -0,0 +1,28 @@ +// Function to register the service worker +export function registerServiceWorker() { + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + const swUrl = `./service-worker.js`; + + navigator.serviceWorker + .register(swUrl) + .then((registration) => { + console.log('Service Worker registered with scope:', registration.scope); + }) + .catch((error) => { + console.error('Service Worker registration failed:', error); + }); + }); + } +} + +// Function to unregister the service worker +export function unregisterServiceWorker() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then((registration) => { + registration.unregister().then((boolean) => { + console.log('Service Worker unregistered:', boolean); + }); + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java new file mode 100644 index 0000000000..32d95115a1 --- /dev/null +++ b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java @@ -0,0 +1,78 @@ +package org.openelisglobal.notifications.entity; + +import javax.persistence.*; +import org.openelisglobal.systemuser.valueholder.SystemUser; + +@Entity +@Table(name = "notification_subscriptions") +public class NotificationSubscription { + + @Id + @Column(name = "user_id") + private Long userId; + + @Column(name = "pf_endpoint", nullable = false) + private String pfEndpoint; + + @Column(name = "pf_p256dh", nullable = false) + private String pfP256dh; + + @Column(name = "pf_auth", nullable = false) + private String pfAuth; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "user_id", nullable = false) + private SystemUser user; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getPfEndpoint() { + return pfEndpoint; + } + + public void setPfEndpoint(String pfEndpoint) { + this.pfEndpoint = pfEndpoint; + } + + public String getPfP256dh() { + return pfP256dh; + } + + public void setPfP256dh(String pfP256dh) { + this.pfP256dh = pfP256dh; + } + + public String getPfAuth() { + return pfAuth; + } + + public void setPfAuth(String pfAuth) { + this.pfAuth = pfAuth; + } + + public SystemUser getUser() { + return user; + } + + public void setUser(SystemUser user) { + this.user = user; + } + + @Override + public String toString() { + return "NotificationSubscription{" + + "userId=" + userId + + ", pfEndpoint='" + pfEndpoint + '\'' + + ", pfP256dh='" + pfP256dh + '\'' + + ", pfAuth='" + pfAuth + '\'' + + ", user=" + (user != null ? user.toString() : "null") + + '}'; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ba6fc4fb5b..02b7c19f20 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -45,3 +45,9 @@ server.ssl.key-password = testtest server.ssl.trust-store=file:/ssl/lf.truststore server.ssl.trust-store-password=testtest +# Push Notification config + +vapid.public.key=BJDIyXHWK_o9fYNwD3fUie2Ed04-yx5fxz9-GUT1c0QhfdDiGMvVbJwvB_On3XapXqIRR471uh7Snw3bfPt9niw +vapid.private.key=FVONpka44MuWq6U8l3X4HY1hAfWM1v1IQB698gsS0KQ + + diff --git a/src/main/resources/liquibase/2.8.x.x/notificationsubsriptions.xml b/src/main/resources/liquibase/2.8.x.x/notificationsubsriptions.xml new file mode 100644 index 0000000000..4d0391eeef --- /dev/null +++ b/src/main/resources/liquibase/2.8.x.x/notificationsubsriptions.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + From fc8f46716b0cef17f07b6940bbec1d349466f3a1 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 8 Aug 2024 11:16:32 +0530 Subject: [PATCH 02/11] create subscriptions table --- .../entity/NotificationSubscription.java | 78 ------------------- .../entity/NotificationSubscriptions.java | 78 +++++++++++++++++++ src/main/resources/liquibase/2.8.x.x/base.xml | 1 + ...ons.xml => notification_subscriptions.xml} | 0 4 files changed, 79 insertions(+), 78 deletions(-) delete mode 100644 src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java create mode 100644 src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java rename src/main/resources/liquibase/2.8.x.x/{notificationsubsriptions.xml => notification_subscriptions.xml} (100%) diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java deleted file mode 100644 index 32d95115a1..0000000000 --- a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscription.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.openelisglobal.notifications.entity; - -import javax.persistence.*; -import org.openelisglobal.systemuser.valueholder.SystemUser; - -@Entity -@Table(name = "notification_subscriptions") -public class NotificationSubscription { - - @Id - @Column(name = "user_id") - private Long userId; - - @Column(name = "pf_endpoint", nullable = false) - private String pfEndpoint; - - @Column(name = "pf_p256dh", nullable = false) - private String pfP256dh; - - @Column(name = "pf_auth", nullable = false) - private String pfAuth; - - @OneToOne(fetch = FetchType.LAZY) - @MapsId - @JoinColumn(name = "user_id", nullable = false) - private SystemUser user; - - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public String getPfEndpoint() { - return pfEndpoint; - } - - public void setPfEndpoint(String pfEndpoint) { - this.pfEndpoint = pfEndpoint; - } - - public String getPfP256dh() { - return pfP256dh; - } - - public void setPfP256dh(String pfP256dh) { - this.pfP256dh = pfP256dh; - } - - public String getPfAuth() { - return pfAuth; - } - - public void setPfAuth(String pfAuth) { - this.pfAuth = pfAuth; - } - - public SystemUser getUser() { - return user; - } - - public void setUser(SystemUser user) { - this.user = user; - } - - @Override - public String toString() { - return "NotificationSubscription{" + - "userId=" + userId + - ", pfEndpoint='" + pfEndpoint + '\'' + - ", pfP256dh='" + pfP256dh + '\'' + - ", pfAuth='" + pfAuth + '\'' + - ", user=" + (user != null ? user.toString() : "null") + - '}'; - } -} diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java new file mode 100644 index 0000000000..444d43c81e --- /dev/null +++ b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java @@ -0,0 +1,78 @@ + package org.openelisglobal.notifications.entity; + + import javax.persistence.*; + import org.openelisglobal.systemuser.valueholder.SystemUser; + + @Entity + @Table(name = "notification_subscriptions") + public class NotificationSubscriptions { + + @Id + @Column(name = "user_id") + private Long userId; + + @Column(name = "pf_endpoint", nullable = false) + private String pfEndpoint; + + @Column(name = "pf_p256dh", nullable = false) + private String pfP256dh; + + @Column(name = "pf_auth", nullable = false) + private String pfAuth; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "user_id", nullable = false) + private SystemUser user; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getPfEndpoint() { + return pfEndpoint; + } + + public void setPfEndpoint(String pfEndpoint) { + this.pfEndpoint = pfEndpoint; + } + + public String getPfP256dh() { + return pfP256dh; + } + + public void setPfP256dh(String pfP256dh) { + this.pfP256dh = pfP256dh; + } + + public String getPfAuth() { + return pfAuth; + } + + public void setPfAuth(String pfAuth) { + this.pfAuth = pfAuth; + } + + public SystemUser getUser() { + return user; + } + + public void setUser(SystemUser user) { + this.user = user; + } + + @Override + public String toString() { + return "NotificationSubscription{" + + "userId=" + userId + + ", pfEndpoint='" + pfEndpoint + '\'' + + ", pfP256dh='" + pfP256dh + '\'' + + ", pfAuth='" + pfAuth + '\'' + + ", user=" + (user != null ? user.toString() : "null") + + '}'; + } + } diff --git a/src/main/resources/liquibase/2.8.x.x/base.xml b/src/main/resources/liquibase/2.8.x.x/base.xml index 4798e21fe9..90ba5390c9 100644 --- a/src/main/resources/liquibase/2.8.x.x/base.xml +++ b/src/main/resources/liquibase/2.8.x.x/base.xml @@ -20,5 +20,6 @@ + diff --git a/src/main/resources/liquibase/2.8.x.x/notificationsubsriptions.xml b/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml similarity index 100% rename from src/main/resources/liquibase/2.8.x.x/notificationsubsriptions.xml rename to src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml From 9fbb2bc4c1fbf1bbeeceb96cfb9b24d002e3effb Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 8 Aug 2024 23:00:06 +0530 Subject: [PATCH 03/11] store pn subscription --- frontend/public/service-worker.js | 42 ++--- .../notifications/SlideOverNotifications.jsx | 124 +++++++++------ .../src/components/notifications/Spinner.jsx | 20 +++ frontend/src/components/utils/Utils.js | 17 ++ frontend/src/index.css | 5 +- frontend/src/index.js | 4 +- frontend/src/serviceWorkerRegistration.js | 43 ++--- .../dao/NotificationSubscriptionDAO.java | 15 ++ .../dao/NotificationSubscriptionDAOImpl.java | 58 +++++++ .../entity/NotificationSubscriptions.java | 150 +++++++++--------- .../rest/NotificationRestController.java | 45 +++++- 11 files changed, 351 insertions(+), 172 deletions(-) create mode 100644 frontend/src/components/notifications/Spinner.jsx create mode 100644 src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java create mode 100644 src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js index 1524edc672..9b54255137 100644 --- a/frontend/public/service-worker.js +++ b/frontend/public/service-worker.js @@ -1,28 +1,30 @@ // Any other custom service worker logic can go here. self.addEventListener("push", async function (event) { - if (event.data) { - const data = event.data.json(); - const notificationOptions = { - body: data.body || "Sample Body", - tag: data.external_id || "default-tag", - }; - - event.waitUntil( - self.registration.showNotification("OpenELIS Test Message Received", notificationOptions) - ); - } + if (event.data) { + const data = event.data.json(); + const notificationOptions = { + body: data.body || "Sample Body", + tag: data.external_id || "default-tag", + }; + + event.waitUntil( + self.registration.showNotification( + "OpenELIS Test Message Received", + notificationOptions, + ), + ); + } }); // This allows the web app to trigger skipWaiting via // registration.waiting.postMessage({type: 'SKIP_WAITING'}) self.addEventListener("message", (event) => { - if (event.data && event.data.type === "SKIP_WAITING") { - self.skipWaiting(); - } + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } }); - // self.addEventListener('push', (e) => { // const data = e.data?.json(); // if (data) { @@ -31,23 +33,23 @@ self.addEventListener("message", (event) => { // }); // } // }); - + // self.addEventListener('notificationclick', (e) => { // e.notification.close(); // e.waitUntil(focusOrOpenWindow()); // }); - + // async function focusOrOpenWindow() { // const url = new URL('/', self.location.origin).href; - + // const allWindows = await self.clients.matchAll({ // type: 'window', // }); // const appWindow = allWindows.find((w) => w.url === url); - + // if (appWindow) { // return appWindow.focus(); // } else { // return self.clients.openWindow(url); // } -// } \ No newline at end of file +// } diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index fccd9a78ad..22d940b6a9 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -1,51 +1,62 @@ import { Renew, NotificationFilled, Email, Filter } from "@carbon/icons-react"; -import { formatTimestamp } from "../utils/Utils"; +import { formatTimestamp, getFromOpenElisServer, getFromOpenElisServerV2, postToOpenElisServer } from "../utils/Utils"; import Spinner from "../common/Sprinner"; import { FormattedMessage, useIntl } from "react-intl"; - - - - - +import { useState } from "react"; + export default function SlideOverNotifications(props) { const intl = useIntl(); + const [iconLoading, setIconLoading] = useState({ + icon: null, + loading: false, + }); async function subscribe() { + try { + setIconLoading({ icon: "NOTIFICATION", loading: true }); + + // Attempt to retrieve the public key from the server + let pbKeyData = await getFromOpenElisServerV2("/rest/notification/public_key"); + + // Attempt to get the service worker registration + const sw = await navigator.serviceWorker.ready; + + // Attempt to subscribe to push notifications + const push = await sw.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: pbKeyData.publicKey, + }); + + // Encode the subscription keys + const p256dh = btoa( + String.fromCharCode.apply(null, new Uint8Array(push.getKey("p256dh"))), + ); + const auth = btoa( + String.fromCharCode.apply(null, new Uint8Array(push.getKey("auth"))), + ); + + // Construct the data object + const data = { + pfEndpoint: push.endpoint, + pfP256dh: p256dh, + pfAuth : auth, + }; + console.log("data",data) - const public_key = "BJDIyXHWK_o9fYNwD3fUie2Ed04-yx5fxz9-GUT1c0QhfdDiGMvVbJwvB_On3XapXqIRR471uh7Snw3bfPt9niw"; - const sw = await navigator.serviceWorker.ready; - const push = await sw.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: public_key, - }); - const p256dh = btoa( - String.fromCharCode.apply( - null, - new Uint8Array(push.getKey("p256dh")), - ), - ); - const auth = btoa( - String.fromCharCode.apply( - null, - new Uint8Array(push.getKey("auth")), - ), - ); - - const data = { - pf_endpoint: push.endpoint, - pf_p256dh: p256dh, - pf_auth: auth, - }; - - console.log("subsription details", data) - - - - + postToOpenElisServer("/rest/notification/subscribe", JSON.stringify(data),(res)=>{ + console.log("res",res); + }); + setIconLoading({ icon: null, loading: false }); + } catch (error) { + // Handle any errors that occurred during the process + console.error("An error occurred during the subscription process:", error); + setIconLoading({ icon: null, loading: false }); + // You can also set an error state here if needed + } } - + const { loading, @@ -99,6 +110,9 @@ export default function SlideOverNotifications(props) { }} >
+ +
+
{[ { - icon: , + icon: iconLoading.loading == true && iconLoading.icon =="RELOAD" ? :, label: intl.formatMessage({ id: "notification.slideover.button.reload", }), - onClick: () => { - getNotifications(); + onClick: async () => { + setIconLoading({ icon: "RELOAD", loading: true }); + await getNotifications(); + setIconLoading({ icon: null, loading: false }); + }, }, { - icon: , + icon: iconLoading.loading == true && iconLoading.icon =="NOTIFICATION" ? :, label: intl.formatMessage({ id: "notification.slideover.button.subscribe", }), - onClick: () => subscribe(), + onClick: async () => { + + await subscribe() + + } }, { - icon: , + icon: iconLoading.loading == true && iconLoading.icon =="EMAIL" ? :, label: intl.formatMessage({ id: "notification.slideover.button.markallasread", - }), - onClick: () => { - markAllNotificationsAsRead(); + }), + onClick:async () => { + setIconLoading({ icon: "EMAIL", loading: true }); + await markAllNotificationsAsRead(); + setIconLoading({ icon: null, loading: false }); }, }, { icon: , label: showRead ? intl.formatMessage({ - id: "notification.slideover.button.hideread", - }) + id: "notification.slideover.button.hideread", + }) : intl.formatMessage({ - id: "notification.slideover.button.showread", - }), + id: "notification.slideover.button.showread", + }), onClick: () => setShowRead(!showRead), }, ].map(({ icon, label, onClick }, index) => ( @@ -150,6 +173,7 @@ export default function SlideOverNotifications(props) { label={label} onClick={onClick} /> + ))}
diff --git a/frontend/src/components/notifications/Spinner.jsx b/frontend/src/components/notifications/Spinner.jsx new file mode 100644 index 0000000000..a9b11516fb --- /dev/null +++ b/frontend/src/components/notifications/Spinner.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const Spinner = () => ( +
+ + + +
+); + +export default Spinner; diff --git a/frontend/src/components/utils/Utils.js b/frontend/src/components/utils/Utils.js index 45f67c9401..c5612c93d0 100644 --- a/frontend/src/components/utils/Utils.js +++ b/frontend/src/components/utils/Utils.js @@ -216,6 +216,23 @@ export const hasRole = (userSessionDetails, role) => { // this is complicated to enable it to format "smartly" as a person types // possible rework could allow it to only format completed numbers + + + +export const getFromOpenElisServerV2 = (url) => { + return new Promise((resolve, reject) => { + // Simulating the original callback-based function + getFromOpenElisServer(url, (res) => { + if (res) { + resolve(res); + } else { + reject(new Error('Failed to fetch data')); + } + }); + }); +}; + + export const convertAlphaNumLabNumForDisplay = (labNumber) => { if (!labNumber) { return labNumber; diff --git a/frontend/src/index.css b/frontend/src/index.css index 76f2bcd213..af31e9c24a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -118,7 +118,6 @@ code { overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); height: 100%; - /* Take full height of the container */ } .slide-over-header { @@ -147,7 +146,6 @@ code { margin: 1px; overflow-y: auto; flex-grow: 1; - /* Allow the body to take remaining height */ } @keyframes pulse { @@ -159,3 +157,6 @@ code { opacity: 0.5; } } + + + diff --git a/frontend/src/index.js b/frontend/src/index.js index 2027da061c..f1727dff56 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,9 +3,9 @@ import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; -import * as ServiceWorker from "./serviceWorkerRegistration" +import * as ServiceWorker from "./serviceWorkerRegistration"; -ServiceWorker.registerServiceWorker() +ServiceWorker.registerServiceWorker(); ReactDOM.render( diff --git a/frontend/src/serviceWorkerRegistration.js b/frontend/src/serviceWorkerRegistration.js index 70ce591f3f..f754ef631d 100644 --- a/frontend/src/serviceWorkerRegistration.js +++ b/frontend/src/serviceWorkerRegistration.js @@ -1,28 +1,31 @@ // Function to register the service worker export function registerServiceWorker() { - if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - const swUrl = `./service-worker.js`; + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + const swUrl = `./service-worker.js`; - navigator.serviceWorker - .register(swUrl) - .then((registration) => { - console.log('Service Worker registered with scope:', registration.scope); - }) - .catch((error) => { - console.error('Service Worker registration failed:', error); - }); + navigator.serviceWorker + .register(swUrl) + .then((registration) => { + console.log( + "Service Worker registered with scope:", + registration.scope, + ); + }) + .catch((error) => { + console.error("Service Worker registration failed:", error); }); - } + }); + } } // Function to unregister the service worker export function unregisterServiceWorker() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then((registration) => { - registration.unregister().then((boolean) => { - console.log('Service Worker unregistered:', boolean); - }); - }); - } -} \ No newline at end of file + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready.then((registration) => { + registration.unregister().then((boolean) => { + console.log("Service Worker unregistered:", boolean); + }); + }); + } +} diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java new file mode 100644 index 0000000000..f6cd509de6 --- /dev/null +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java @@ -0,0 +1,15 @@ +package org.openelisglobal.notifications.dao; + +import org.openelisglobal.notifications.entity.NotificationSubscriptions; + +public interface NotificationSubscriptionDAO { + + void save(NotificationSubscriptions notificationSubscription); + + NotificationSubscriptions getNotificationSubscriptionById(String id); + + void updateNotificationSubscription(NotificationSubscriptions notificationSubscription); + + void saveOrUpdate(NotificationSubscriptions notificationSubscription); + +} \ No newline at end of file diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java new file mode 100644 index 0000000000..0acd68712b --- /dev/null +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java @@ -0,0 +1,58 @@ +package org.openelisglobal.notifications.dao; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; +import org.openelisglobal.notifications.entity.NotificationSubscriptions; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public class NotificationSubscriptionDAOImpl implements NotificationSubscriptionDAO { + + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public void save(NotificationSubscriptions notificationSubscription) { + entityManager.merge(notificationSubscription); + } + + @Override + public NotificationSubscriptions getNotificationSubscriptionById(String id) { + try { + return entityManager.find(NotificationSubscriptions.class, id); + } catch (NoResultException e) { + return null; + } + } + + @Override + @Transactional + public void updateNotificationSubscription(NotificationSubscriptions notificationSubscription) { + entityManager.merge(notificationSubscription); + } + + // Update saveOrUpdate method +@Transactional +public void saveOrUpdate(NotificationSubscriptions notificationSubscription) { + if (notificationSubscription.getUser() == null) { + throw new IllegalArgumentException("UserId must be provided"); + } + + NotificationSubscriptions existingSubscription = getNotificationSubscriptionById(notificationSubscription.getUser().getId()); + + if (existingSubscription == null) { + // Create a new subscription + entityManager.persist(notificationSubscription); // Use persist() for new entities + } else { + // Update the existing subscription + existingSubscription.setPfEndpoint(notificationSubscription.getPfEndpoint()); + existingSubscription.setPfP256dh(notificationSubscription.getPfP256dh()); + existingSubscription.setPfAuth(notificationSubscription.getPfAuth()); + entityManager.merge(existingSubscription); // Use merge() for existing entities + } +} + +} diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java index 444d43c81e..3efbbd8962 100644 --- a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java +++ b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java @@ -1,78 +1,74 @@ - package org.openelisglobal.notifications.entity; - - import javax.persistence.*; - import org.openelisglobal.systemuser.valueholder.SystemUser; - - @Entity - @Table(name = "notification_subscriptions") - public class NotificationSubscriptions { - - @Id - @Column(name = "user_id") - private Long userId; - - @Column(name = "pf_endpoint", nullable = false) - private String pfEndpoint; - - @Column(name = "pf_p256dh", nullable = false) - private String pfP256dh; - - @Column(name = "pf_auth", nullable = false) - private String pfAuth; - - @OneToOne(fetch = FetchType.LAZY) - @MapsId - @JoinColumn(name = "user_id", nullable = false) - private SystemUser user; - - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public String getPfEndpoint() { - return pfEndpoint; - } - - public void setPfEndpoint(String pfEndpoint) { - this.pfEndpoint = pfEndpoint; - } - - public String getPfP256dh() { - return pfP256dh; - } - - public void setPfP256dh(String pfP256dh) { - this.pfP256dh = pfP256dh; - } - - public String getPfAuth() { - return pfAuth; - } - - public void setPfAuth(String pfAuth) { - this.pfAuth = pfAuth; - } - - public SystemUser getUser() { - return user; - } - - public void setUser(SystemUser user) { - this.user = user; - } - - @Override - public String toString() { - return "NotificationSubscription{" + - "userId=" + userId + - ", pfEndpoint='" + pfEndpoint + '\'' + - ", pfP256dh='" + pfP256dh + '\'' + - ", pfAuth='" + pfAuth + '\'' + - ", user=" + (user != null ? user.toString() : "null") + - '}'; - } +package org.openelisglobal.notifications.entity; + +import javax.persistence.*; +import org.openelisglobal.systemuser.valueholder.SystemUser; + +@Entity +@Table(name = "notification_subscriptions") +public class NotificationSubscriptions { + + @Id + @Column(name = "user_id") + private String userId; + + @Column(name = "pf_endpoint", nullable = false) + private String pfEndpoint; + + @Column(name = "pf_p256dh", nullable = false) + private String pfP256dh; + + @Column(name = "pf_auth", nullable = false) + private String pfAuth; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "user_id", nullable = false) + private SystemUser user; + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getPfEndpoint() { + return pfEndpoint; + } + + public void setPfEndpoint(String pfEndpoint) { + this.pfEndpoint = pfEndpoint; + } + + public String getPfP256dh() { + return pfP256dh; + } + + public void setPfP256dh(String pfP256dh) { + this.pfP256dh = pfP256dh; + } + + public String getPfAuth() { + return pfAuth; + } + + public void setPfAuth(String pfAuth) { + this.pfAuth = pfAuth; + } + + public SystemUser getUser() { + return user; + } + + public void setUser(SystemUser user) { + this.user = user; + } + + @Override + public String toString() { + return "NotificationSubscription{" + "userId=" + userId + ", pfEndpoint='" + pfEndpoint + '\'' + ", pfP256dh='" + + pfP256dh + '\'' + ", pfAuth='" + pfAuth + '\'' + ", user=" + (user != null ? user.toString() : "null") + + '}'; } +} diff --git a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java index 7a6e9010fd..fa2025a46c 100644 --- a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java +++ b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java @@ -1,14 +1,19 @@ package org.openelisglobal.notifications.rest; import java.time.OffsetDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.openelisglobal.login.valueholder.UserSessionData; import org.openelisglobal.notifications.dao.NotificationDAO; +import org.openelisglobal.notifications.dao.NotificationSubscriptionDAO; import org.openelisglobal.notifications.entity.Notification; +import org.openelisglobal.notifications.entity.NotificationSubscriptions; import org.openelisglobal.systemuser.service.SystemUserService; import org.openelisglobal.systemuser.valueholder.SystemUser; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -24,12 +29,18 @@ public class NotificationRestController { private final NotificationDAO notificationDAO; private final SystemUserService systemUserService; + private final NotificationSubscriptionDAO notificationSubscriptionDAO; private static final String USER_SESSION_DATA = "userSessionData"; @Autowired - public NotificationRestController(NotificationDAO notificationDAO, SystemUserService systemUserService) { + private ConfigurableEnvironment env; + + @Autowired + public NotificationRestController(NotificationDAO notificationDAO, SystemUserService systemUserService, + NotificationSubscriptionDAO notificationSubscriptionDAO) { this.notificationDAO = notificationDAO; this.systemUserService = systemUserService; + this.notificationSubscriptionDAO = notificationSubscriptionDAO; } @GetMapping("/notifications/all") @@ -72,6 +83,38 @@ public List getSystemUsers() { return notificationDAO.getSystemUsers(); } + @GetMapping("/notification/public_key") + public ResponseEntity> getPublicKey() { + + String publicKey = env.getProperty("vapid.public.key"); + + Map response = new HashMap<>(); + response.put("publicKey", publicKey); + return ResponseEntity.ok().body(response); + } + + @PostMapping("/notification/subscribe") + public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notificationSubscription, + HttpServletRequest request) { + + String sysUserId = getSysUserId(request); + + notificationSubscription.setUserId(systemUserService.getUserById(sysUserId).getSysUserId()); + + System.out.println("userId 1" + sysUserId); + System.out.println("User ID 2: " + systemUserService.getUserById(sysUserId).getSysUserId()); + System.out.println("User ID 3 " + systemUserService.getUserById(sysUserId).getId()); + + System.out.println("Endpoint: " + notificationSubscription.getPfEndpoint()); + System.out.println("P256dh: " + notificationSubscription.getPfP256dh()); + System.out.println("Auth: " + notificationSubscription.getPfAuth()); + System.out.println("User: " + notificationSubscription.getUser()); + System.out.println("User ID: " + notificationSubscription.toString()); + notificationSubscriptionDAO.saveOrUpdate(notificationSubscription); + + return ResponseEntity.ok().body("Subscribed successfully"); + } + protected String getSysUserId(HttpServletRequest request) { UserSessionData usd = (UserSessionData) request.getSession().getAttribute(USER_SESSION_DATA); if (usd == null) { From 3f638af575d0a6c50e6fa6e8edaa9b4265221e68 Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 9 Aug 2024 16:03:32 +0530 Subject: [PATCH 04/11] fix errors --- .../dao/NotificationSubscriptionDAO.java | 2 +- .../dao/NotificationSubscriptionDAOImpl.java | 10 ++--- .../entity/NotificationSubscriptions.java | 32 ++++++++-------- .../rest/NotificationRestController.java | 38 +++++++++++-------- .../2.8.x.x/notification_subscriptions.xml | 7 +++- 5 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java index f6cd509de6..0fa45d8d4d 100644 --- a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java @@ -6,7 +6,7 @@ public interface NotificationSubscriptionDAO { void save(NotificationSubscriptions notificationSubscription); - NotificationSubscriptions getNotificationSubscriptionById(String id); + NotificationSubscriptions getNotificationSubscriptionById(Long id); void updateNotificationSubscription(NotificationSubscriptions notificationSubscription); diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java index 0acd68712b..f809217135 100644 --- a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java @@ -16,11 +16,11 @@ public class NotificationSubscriptionDAOImpl implements NotificationSubscription @Override @Transactional public void save(NotificationSubscriptions notificationSubscription) { - entityManager.merge(notificationSubscription); + entityManager.persist(notificationSubscription); } @Override - public NotificationSubscriptions getNotificationSubscriptionById(String id) { + public NotificationSubscriptions getNotificationSubscriptionById(Long id) { try { return entityManager.find(NotificationSubscriptions.class, id); } catch (NoResultException e) { @@ -37,11 +37,9 @@ public void updateNotificationSubscription(NotificationSubscriptions notificatio // Update saveOrUpdate method @Transactional public void saveOrUpdate(NotificationSubscriptions notificationSubscription) { - if (notificationSubscription.getUser() == null) { - throw new IllegalArgumentException("UserId must be provided"); - } + - NotificationSubscriptions existingSubscription = getNotificationSubscriptionById(notificationSubscription.getUser().getId()); + NotificationSubscriptions existingSubscription = getNotificationSubscriptionById(Long.valueOf(notificationSubscription.getUser().getId())); if (existingSubscription == null) { // Create a new subscription diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java index 3efbbd8962..34591775d5 100644 --- a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java +++ b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java @@ -8,8 +8,9 @@ public class NotificationSubscriptions { @Id - @Column(name = "user_id") - private String userId; + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; @Column(name = "pf_endpoint", nullable = false) private String pfEndpoint; @@ -21,16 +22,23 @@ public class NotificationSubscriptions { private String pfAuth; @OneToOne(fetch = FetchType.LAZY) - @MapsId @JoinColumn(name = "user_id", nullable = false) private SystemUser user; - public String getUserId() { - return userId; + public Long getId() { + return id; } - public void setUserId(String userId) { - this.userId = userId; + public void setId(Long id) { + this.id = id; + } + + public SystemUser getUser() { + return user; + } + + public void setUser(SystemUser user) { + this.user = user; } public String getPfEndpoint() { @@ -57,17 +65,11 @@ public void setPfAuth(String pfAuth) { this.pfAuth = pfAuth; } - public SystemUser getUser() { - return user; - } - - public void setUser(SystemUser user) { - this.user = user; - } + @Override public String toString() { - return "NotificationSubscription{" + "userId=" + userId + ", pfEndpoint='" + pfEndpoint + '\'' + ", pfP256dh='" + return "NotificationSubscription{" + "userId=" + ", pfEndpoint='" + pfEndpoint + '\'' + ", pfP256dh='" + pfP256dh + '\'' + ", pfAuth='" + pfAuth + '\'' + ", user=" + (user != null ? user.toString() : "null") + '}'; } diff --git a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java index fa2025a46c..4a635419b4 100644 --- a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java +++ b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java @@ -14,6 +14,7 @@ import org.openelisglobal.systemuser.valueholder.SystemUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -94,26 +95,33 @@ public ResponseEntity> getPublicKey() { } @PostMapping("/notification/subscribe") - public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notificationSubscription, - HttpServletRequest request) { +public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notificationSubscription, + HttpServletRequest request) { - String sysUserId = getSysUserId(request); + String sysUserId = getSysUserId(request); + + // Fetch the user object + SystemUser user = systemUserService.getUserById(sysUserId); - notificationSubscription.setUserId(systemUserService.getUserById(sysUserId).getSysUserId()); + // Ensure user object is not null + if (user == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); + } - System.out.println("userId 1" + sysUserId); - System.out.println("User ID 2: " + systemUserService.getUserById(sysUserId).getSysUserId()); - System.out.println("User ID 3 " + systemUserService.getUserById(sysUserId).getId()); + // Set the user entity directly + notificationSubscription.setUser(user); + + System.out.println("userId 1 " + sysUserId); + System.out.println("User ID 3 " + user.getId()); + System.out.println("User ID 4 from notification " + notificationSubscription.getUser().getId()); - System.out.println("Endpoint: " + notificationSubscription.getPfEndpoint()); - System.out.println("P256dh: " + notificationSubscription.getPfP256dh()); - System.out.println("Auth: " + notificationSubscription.getPfAuth()); - System.out.println("User: " + notificationSubscription.getUser()); - System.out.println("User ID: " + notificationSubscription.toString()); - notificationSubscriptionDAO.saveOrUpdate(notificationSubscription); + notificationSubscriptionDAO.saveOrUpdate(notificationSubscription); - return ResponseEntity.ok().body("Subscribed successfully"); - } + return ResponseEntity.ok().body("Subscribed successfully"); +} + + + protected String getSysUserId(HttpServletRequest request) { UserSessionData usd = (UserSessionData) request.getSession().getAttribute(USER_SESSION_DATA); diff --git a/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml b/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml index 4d0391eeef..77e8ca0790 100644 --- a/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml +++ b/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml @@ -5,8 +5,8 @@ - - + + @@ -17,6 +17,9 @@ + + + Date: Tue, 20 Aug 2024 12:54:47 +0530 Subject: [PATCH 05/11] v3 --- frontend/public/service-worker.js | 111 ++++++++++------- .../notifications/SlideOverNotifications.jsx | 78 +++++++++--- pom.xml | 13 +- .../dao/NotificationSubscriptionDAO.java | 2 +- .../dao/NotificationSubscriptionDAOImpl.java | 42 ++++--- .../entity/NotificationSubscriptions.java | 35 +++++- .../rest/NotificationRestController.java | 114 ++++++++++++++---- 7 files changed, 288 insertions(+), 107 deletions(-) diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js index 9b54255137..cd3cf24654 100644 --- a/frontend/public/service-worker.js +++ b/frontend/public/service-worker.js @@ -1,55 +1,84 @@ -// Any other custom service worker logic can go here. +// Define a cache name for versioning your cache +const CACHE_NAME = 'my-cache-v1'; -self.addEventListener("push", async function (event) { +// Cache assets during the install phase +self.addEventListener('install', (event) => { + console.log('[Service Worker] Install'); + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll([ + '/', + '/index.html', + '/styles.css', + ]); + }).then(() => self.skipWaiting()) // Skip waiting to activate new service worker immediately + ); +}); + +// Clean up old caches during the activate phase +self.addEventListener('activate', (event) => { + console.log('[Service Worker] Activate'); + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log('[Service Worker] Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }).then(() => self.clients.claim()) // Take control of all clients as soon as active + ); +}); + + + +// Listen for push events and display notifications +self.addEventListener('push', (event) => { + console.log('[Service Worker] Push Received', event); if (event.data) { const data = event.data.json(); const notificationOptions = { - body: data.body || "Sample Body", - tag: data.external_id || "default-tag", + body:data.body || 'Message Received from OpenELIS', + tag: data.external_id || 'default-tag', + icon: 'images/openelis_logo.png', }; event.waitUntil( - self.registration.showNotification( - "OpenELIS Test Message Received", - notificationOptions, - ), + self.registration.showNotification('OpenELIS Test Message Received', notificationOptions) ); } }); -// This allows the web app to trigger skipWaiting via -// registration.waiting.postMessage({type: 'SKIP_WAITING'}) -self.addEventListener("message", (event) => { - if (event.data && event.data.type === "SKIP_WAITING") { +// Notification click event listener +// self.addEventListener("notificationclick", (event) => { +// console.log('Notification clicked'); +// event.notification.close(); // Close the notification popout + +// event.waitUntil( +// clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { +// // Check if any client (tab or window) is already open +// for (let i = 0; i < clientList.length; i++) { +// const client = clientList[i]; +// if (client.url === 'https://www.youtube.com/' && 'focus' in client) { +// return client.focus(); +// } +// } + +// // If no client is open, open a new window +// if (clients.openWindow) { +// return clients.openWindow('https://www.youtube.com/'); +// } +// }) +// ); +// }); + + + +// Handle messages from clients +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); - -// self.addEventListener('push', (e) => { -// const data = e.data?.json(); -// if (data) { -// self.registration.showNotification(data.title, { -// body: data.body, -// }); -// } -// }); - -// self.addEventListener('notificationclick', (e) => { -// e.notification.close(); -// e.waitUntil(focusOrOpenWindow()); -// }); - -// async function focusOrOpenWindow() { -// const url = new URL('/', self.location.origin).href; - -// const allWindows = await self.clients.matchAll({ -// type: 'window', -// }); -// const appWindow = allWindows.find((w) => w.url === url); - -// if (appWindow) { -// return appWindow.focus(); -// } else { -// return self.clients.openWindow(url); -// } -// } diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index 22d940b6a9..4fa407e185 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -13,49 +13,89 @@ export default function SlideOverNotifications(props) { async function subscribe() { try { + // Set the loading state setIconLoading({ icon: "NOTIFICATION", loading: true }); - // Attempt to retrieve the public key from the server - let pbKeyData = await getFromOpenElisServerV2("/rest/notification/public_key"); + // Check if service workers are supported + if (!('serviceWorker' in navigator)) { + throw new Error("Service workers are not supported in this browser."); + } + + // Check if push messaging is supported + if (!('PushManager' in window)) { + throw new Error("Push messaging is not supported in this browser."); + } + + // Register the service worker if not already registered + const registration = await navigator.serviceWorker.register('/service-worker.js').catch(error => { + throw new Error("Service worker registration failed: " + error.message); + }); - // Attempt to get the service worker registration + // Ensure the service worker is ready const sw = await navigator.serviceWorker.ready; + // Attempt to retrieve the public key from the server + let pbKeyData = await getFromOpenElisServerV2("/rest/notification/public_key").catch(error => { + throw new Error("Failed to retrieve public key from server: " + error.message); + }); + + // Convert the public key to a Uint8Array + const applicationServerKey = urlBase64ToUint8Array(pbKeyData.publicKey); + // Attempt to subscribe to push notifications const push = await sw.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: pbKeyData.publicKey, + applicationServerKey, + }).catch(error => { + throw new Error("Push subscription failed: " + error.message); }); // Encode the subscription keys - const p256dh = btoa( - String.fromCharCode.apply(null, new Uint8Array(push.getKey("p256dh"))), - ); - const auth = btoa( - String.fromCharCode.apply(null, new Uint8Array(push.getKey("auth"))), - ); + const p256dh = btoa(String.fromCharCode.apply(null, new Uint8Array(push.getKey("p256dh")))); + const auth = btoa(String.fromCharCode.apply(null, new Uint8Array(push.getKey("auth")))); // Construct the data object const data = { pfEndpoint: push.endpoint, pfP256dh: p256dh, - pfAuth : auth, + pfAuth: auth, }; - - console.log("data",data) - - postToOpenElisServer("/rest/notification/subscribe", JSON.stringify(data),(res)=>{ - console.log("res",res); + + console.log("data", data); + + // Send the subscription data to the server + postToOpenElisServer("/rest/notification/subscribe", JSON.stringify(data), (res) => { + console.log("res", res); }); - + + // Set the loading state to false setIconLoading({ icon: null, loading: false }); } catch (error) { // Handle any errors that occurred during the process console.error("An error occurred during the subscription process:", error); setIconLoading({ icon: null, loading: false }); - // You can also set an error state here if needed + // Optionally set an error state here or provide user feedback } } + + + + // Helper function to convert a URL-safe base64 string to a Uint8Array + function urlBase64ToUint8Array(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; + } + const { @@ -112,7 +152,7 @@ export default function SlideOverNotifications(props) {

- +
hibernate-search-backend-lucene ${hibernate-search.version} + + + nl.martijndwars + web-push + 5.1.1 + + + org.bouncycastle + bcpkix-jdk18on + 1.78 + javax.annotation @@ -681,4 +692,4 @@ - + \ No newline at end of file diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java index 0fa45d8d4d..f536e35368 100644 --- a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java @@ -6,7 +6,7 @@ public interface NotificationSubscriptionDAO { void save(NotificationSubscriptions notificationSubscription); - NotificationSubscriptions getNotificationSubscriptionById(Long id); + NotificationSubscriptions getNotificationSubscriptionByUserId(Long id); void updateNotificationSubscription(NotificationSubscriptions notificationSubscription); diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java index f809217135..a38c0d1a07 100644 --- a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java @@ -1,7 +1,6 @@ package org.openelisglobal.notifications.dao; import javax.persistence.EntityManager; -import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import org.openelisglobal.notifications.entity.NotificationSubscriptions; import org.springframework.stereotype.Repository; @@ -20,12 +19,15 @@ public void save(NotificationSubscriptions notificationSubscription) { } @Override - public NotificationSubscriptions getNotificationSubscriptionById(Long id) { + public NotificationSubscriptions getNotificationSubscriptionByUserId(Long id) { + try { - return entityManager.find(NotificationSubscriptions.class, id); - } catch (NoResultException e) { + return entityManager.createQuery("SELECT ns FROM NotificationSubscriptions ns WHERE ns.user.id = :id", + NotificationSubscriptions.class).setParameter("id", id).getSingleResult(); + } catch (Exception e) { return null; } + } @Override @@ -35,22 +37,22 @@ public void updateNotificationSubscription(NotificationSubscriptions notificatio } // Update saveOrUpdate method -@Transactional -public void saveOrUpdate(NotificationSubscriptions notificationSubscription) { - - - NotificationSubscriptions existingSubscription = getNotificationSubscriptionById(Long.valueOf(notificationSubscription.getUser().getId())); - - if (existingSubscription == null) { - // Create a new subscription - entityManager.persist(notificationSubscription); // Use persist() for new entities - } else { - // Update the existing subscription - existingSubscription.setPfEndpoint(notificationSubscription.getPfEndpoint()); - existingSubscription.setPfP256dh(notificationSubscription.getPfP256dh()); - existingSubscription.setPfAuth(notificationSubscription.getPfAuth()); - entityManager.merge(existingSubscription); // Use merge() for existing entities + @Transactional + public void saveOrUpdate(NotificationSubscriptions notificationSubscription) { + + NotificationSubscriptions existingSubscription = getNotificationSubscriptionByUserId( + Long.valueOf(notificationSubscription.getUser().getId())); + + if (existingSubscription == null) { + // Create a new subscription + entityManager.persist(notificationSubscription); // Use persist() for new entities + } else { + // Update the existing subscription + existingSubscription.setPfEndpoint(notificationSubscription.getPfEndpoint()); + existingSubscription.setPfP256dh(notificationSubscription.getPfP256dh()); + existingSubscription.setPfAuth(notificationSubscription.getPfAuth()); + entityManager.merge(existingSubscription); // Use merge() for existing entities + } } -} } diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java index 34591775d5..88c81429e7 100644 --- a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java +++ b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java @@ -25,6 +25,13 @@ public class NotificationSubscriptions { @JoinColumn(name = "user_id", nullable = false) private SystemUser user; + // Transient fields + @Transient + private String title; + + @Transient + private String message; + public Long getId() { return id; } @@ -65,12 +72,32 @@ public void setPfAuth(String pfAuth) { this.pfAuth = pfAuth; } - + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } @Override public String toString() { - return "NotificationSubscription{" + "userId=" + ", pfEndpoint='" + pfEndpoint + '\'' + ", pfP256dh='" - + pfP256dh + '\'' + ", pfAuth='" + pfAuth + '\'' + ", user=" + (user != null ? user.toString() : "null") - + '}'; + return "NotificationSubscription{" + + "userId=" + (user != null ? user.getId() : "null") + + ", pfEndpoint='" + pfEndpoint + '\'' + + ", pfP256dh='" + pfP256dh + '\'' + + ", pfAuth='" + pfAuth + '\'' + + ", user=" + (user != null ? user.toString() : "null") + + ", title='" + title + '\'' + + ", message='" + message + '\'' + + '}'; } } diff --git a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java index 4a635419b4..392d014b1f 100644 --- a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java +++ b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java @@ -1,10 +1,14 @@ package org.openelisglobal.notifications.rest; +import java.security.Security; import java.time.OffsetDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; + +import org.apache.http.HttpResponse; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.openelisglobal.login.valueholder.UserSessionData; import org.openelisglobal.notifications.dao.NotificationDAO; import org.openelisglobal.notifications.dao.NotificationSubscriptionDAO; @@ -23,6 +27,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import nl.martijndwars.webpush.PushService; + +import com.fasterxml.jackson.databind.ObjectMapper; @RequestMapping("/rest") @RestController @@ -61,9 +68,77 @@ public ResponseEntity saveNotification(@PathVariable String userId, @RequestB notification.setCreatedDate(OffsetDateTime.now()); notification.setReadAt(null); notificationDAO.save(notification); - return ResponseEntity.ok().body("Notification saved successfully"); + + // Ensure BouncyCastleProvider is added for cryptographic operations + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + NotificationSubscriptions ns = notificationSubscriptionDAO + .getNotificationSubscriptionByUserId(Long.valueOf(userId)); + + if (ns == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Subscription not found"); + } + + try { + // Configure PushService with VAPID keys + PushService pushService = new PushService( + env.getProperty("vapid.public.key"), + env.getProperty("vapid.private.key"), + "mailto:your-email@example.com"); + + String title = "OpenELIS Global Notification"; + String body = notification.getMessage(); + String url = "http://localhost"; + + // Create a notification message + Map payload = new HashMap<>(); + payload.put("title", title); + payload.put("body", body); + payload.put("url", url); + + ObjectMapper objectMapper = new ObjectMapper(); + String payloadJson = objectMapper.writeValueAsString(payload); + + // Use fully qualified name for web push Notification + nl.martijndwars.webpush.Notification webPushNotification = new nl.martijndwars.webpush.Notification( + ns.getPfEndpoint(), + ns.getPfP256dh(), + ns.getPfAuth(), + payloadJson); + + HttpResponse response = pushService.send(webPushNotification); + + return ResponseEntity.ok() + .body("Push notification sent successfully. Response: " + response.getStatusLine()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to send push notification: " + e.getMessage()); + } + } + // @GetMapping("/notification/testpush") + // public ResponseEntity testPushNotification() { + + // // Fetch user and subscription + // SystemUser user = systemUserService.getUserById("111"); + // NotificationSubscriptions ns = + // notificationSubscriptionDAO.getNotificationSubscriptionByUserId(Long.valueOf(111)); + + // String title = "Test Notification"; + // String body = "This is a test notification"; + // String url = "https://example.com"; + + // if (ns.getPfAuth() == null || ns.getPfP256dh() == null || ns.getPfEndpoint() + // == null) { + // return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Subscription not + // found"); + // } + + // } + @PutMapping("/notification/markasread/{id}") public ResponseEntity markNotificationAsRead(@PathVariable String id) { Notification notification = notificationDAO.getNotificationById(Long.parseLong(id)); @@ -95,33 +170,30 @@ public ResponseEntity> getPublicKey() { } @PostMapping("/notification/subscribe") -public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notificationSubscription, - HttpServletRequest request) { - - String sysUserId = getSysUserId(request); + public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notificationSubscription, + HttpServletRequest request) { - // Fetch the user object - SystemUser user = systemUserService.getUserById(sysUserId); + String sysUserId = getSysUserId(request); - // Ensure user object is not null - if (user == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); - } + // Fetch the user object + SystemUser user = systemUserService.getUserById(sysUserId); - // Set the user entity directly - notificationSubscription.setUser(user); - - System.out.println("userId 1 " + sysUserId); - System.out.println("User ID 3 " + user.getId()); - System.out.println("User ID 4 from notification " + notificationSubscription.getUser().getId()); + // Ensure user object is not null + if (user == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); + } - notificationSubscriptionDAO.saveOrUpdate(notificationSubscription); + // Set the user entity directly + notificationSubscription.setUser(user); - return ResponseEntity.ok().body("Subscribed successfully"); -} + System.out.println("userId 1 " + sysUserId); + System.out.println("User ID 3 " + user.getId()); + System.out.println("User ID 4 from notification " + notificationSubscription.getUser().getId()); + notificationSubscriptionDAO.saveOrUpdate(notificationSubscription); - + return ResponseEntity.ok().body("Subscribed successfully"); + } protected String getSysUserId(HttpServletRequest request) { UserSessionData usd = (UserSessionData) request.getSession().getAttribute(USER_SESSION_DATA); From 92e057939d9f49a1004ee5e6cf4b5595b283c09f Mon Sep 17 00:00:00 2001 From: vivek Date: Tue, 20 Aug 2024 13:43:06 +0530 Subject: [PATCH 06/11] format --- frontend/public/service-worker.js | 69 ++++----- frontend/src/components/Style.css | 2 +- .../notifications/SlideOverNotifications.jsx | 143 +++++++++++------- .../src/components/notifications/Spinner.jsx | 11 +- frontend/src/components/utils/Utils.js | 5 +- frontend/src/index.css | 3 - pom.xml | 2 +- .../entity/NotificationSubscriptions.java | 13 +- .../rest/NotificationRestController.java | 17 +-- 9 files changed, 140 insertions(+), 125 deletions(-) diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js index cd3cf24654..effbde732a 100644 --- a/frontend/public/service-worker.js +++ b/frontend/public/service-worker.js @@ -1,52 +1,55 @@ // Define a cache name for versioning your cache -const CACHE_NAME = 'my-cache-v1'; +const CACHE_NAME = "my-cache-v1"; // Cache assets during the install phase -self.addEventListener('install', (event) => { - console.log('[Service Worker] Install'); +self.addEventListener("install", (event) => { + console.log("[Service Worker] Install"); event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - return cache.addAll([ - '/', - '/index.html', - '/styles.css', - ]); - }).then(() => self.skipWaiting()) // Skip waiting to activate new service worker immediately + caches + .open(CACHE_NAME) + .then((cache) => { + return cache.addAll(["/", "/index.html", "/styles.css"]); + }) + .then(() => self.skipWaiting()), // Skip waiting to activate new service worker immediately ); }); // Clean up old caches during the activate phase -self.addEventListener('activate', (event) => { - console.log('[Service Worker] Activate'); +self.addEventListener("activate", (event) => { + console.log("[Service Worker] Activate"); event.waitUntil( - caches.keys().then((cacheNames) => { - return Promise.all( - cacheNames.map((cacheName) => { - if (cacheName !== CACHE_NAME) { - console.log('[Service Worker] Deleting old cache:', cacheName); - return caches.delete(cacheName); - } - }) - ); - }).then(() => self.clients.claim()) // Take control of all clients as soon as active + caches + .keys() + .then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log("[Service Worker] Deleting old cache:", cacheName); + return caches.delete(cacheName); + } + }), + ); + }) + .then(() => self.clients.claim()), // Take control of all clients as soon as active ); }); - - // Listen for push events and display notifications -self.addEventListener('push', (event) => { - console.log('[Service Worker] Push Received', event); +self.addEventListener("push", (event) => { + console.log("[Service Worker] Push Received", event); if (event.data) { const data = event.data.json(); const notificationOptions = { - body:data.body || 'Message Received from OpenELIS', - tag: data.external_id || 'default-tag', - icon: 'images/openelis_logo.png', + body: data.body || "Message Received from OpenELIS", + tag: data.external_id || "default-tag", + icon: "images/openelis_logo.png", }; event.waitUntil( - self.registration.showNotification('OpenELIS Test Message Received', notificationOptions) + self.registration.showNotification( + "OpenELIS Test Message Received", + notificationOptions, + ), ); } }); @@ -74,11 +77,9 @@ self.addEventListener('push', (event) => { // ); // }); - - // Handle messages from clients -self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') { +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { self.skipWaiting(); } }); diff --git a/frontend/src/components/Style.css b/frontend/src/components/Style.css index d1f629f5b6..d9e8fefcb3 100644 --- a/frontend/src/components/Style.css +++ b/frontend/src/components/Style.css @@ -508,7 +508,7 @@ button { } #mainHeader .cds--side-nav__link { - pointer-events: none + pointer-events: none; } @media screen and (max-width: 792px) { diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index 4fa407e185..4656f1f782 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -1,9 +1,14 @@ import { Renew, NotificationFilled, Email, Filter } from "@carbon/icons-react"; -import { formatTimestamp, getFromOpenElisServer, getFromOpenElisServerV2, postToOpenElisServer } from "../utils/Utils"; +import { + formatTimestamp, + getFromOpenElisServer, + getFromOpenElisServerV2, + postToOpenElisServer, +} from "../utils/Utils"; import Spinner from "../common/Sprinner"; import { FormattedMessage, useIntl } from "react-intl"; import { useState } from "react"; - + export default function SlideOverNotifications(props) { const intl = useIntl(); const [iconLoading, setIconLoading] = useState({ @@ -15,88 +20,105 @@ export default function SlideOverNotifications(props) { try { // Set the loading state setIconLoading({ icon: "NOTIFICATION", loading: true }); - + // Check if service workers are supported - if (!('serviceWorker' in navigator)) { + if (!("serviceWorker" in navigator)) { throw new Error("Service workers are not supported in this browser."); } - + // Check if push messaging is supported - if (!('PushManager' in window)) { + if (!("PushManager" in window)) { throw new Error("Push messaging is not supported in this browser."); } - + // Register the service worker if not already registered - const registration = await navigator.serviceWorker.register('/service-worker.js').catch(error => { - throw new Error("Service worker registration failed: " + error.message); - }); - + const registration = await navigator.serviceWorker + .register("/service-worker.js") + .catch((error) => { + throw new Error( + "Service worker registration failed: " + error.message, + ); + }); + // Ensure the service worker is ready const sw = await navigator.serviceWorker.ready; - + // Attempt to retrieve the public key from the server - let pbKeyData = await getFromOpenElisServerV2("/rest/notification/public_key").catch(error => { - throw new Error("Failed to retrieve public key from server: " + error.message); + let pbKeyData = await getFromOpenElisServerV2( + "/rest/notification/public_key", + ).catch((error) => { + throw new Error( + "Failed to retrieve public key from server: " + error.message, + ); }); - + // Convert the public key to a Uint8Array const applicationServerKey = urlBase64ToUint8Array(pbKeyData.publicKey); - + // Attempt to subscribe to push notifications - const push = await sw.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey, - }).catch(error => { - throw new Error("Push subscription failed: " + error.message); - }); - + const push = await sw.pushManager + .subscribe({ + userVisibleOnly: true, + applicationServerKey, + }) + .catch((error) => { + throw new Error("Push subscription failed: " + error.message); + }); + // Encode the subscription keys - const p256dh = btoa(String.fromCharCode.apply(null, new Uint8Array(push.getKey("p256dh")))); - const auth = btoa(String.fromCharCode.apply(null, new Uint8Array(push.getKey("auth")))); - + const p256dh = btoa( + String.fromCharCode.apply(null, new Uint8Array(push.getKey("p256dh"))), + ); + const auth = btoa( + String.fromCharCode.apply(null, new Uint8Array(push.getKey("auth"))), + ); + // Construct the data object const data = { pfEndpoint: push.endpoint, pfP256dh: p256dh, pfAuth: auth, }; - + console.log("data", data); - + // Send the subscription data to the server - postToOpenElisServer("/rest/notification/subscribe", JSON.stringify(data), (res) => { - console.log("res", res); - }); - + postToOpenElisServer( + "/rest/notification/subscribe", + JSON.stringify(data), + (res) => { + console.log("res", res); + }, + ); + // Set the loading state to false setIconLoading({ icon: null, loading: false }); } catch (error) { // Handle any errors that occurred during the process - console.error("An error occurred during the subscription process:", error); + console.error( + "An error occurred during the subscription process:", + error, + ); setIconLoading({ icon: null, loading: false }); // Optionally set an error state here or provide user feedback } } - - // Helper function to convert a URL-safe base64 string to a Uint8Array function urlBase64ToUint8Array(base64String) { - const padding = '='.repeat((4 - base64String.length % 4) % 4); + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding) - .replace(/-/g, '+') - .replace(/_/g, '/'); - + .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; } - - const { loading, @@ -150,9 +172,8 @@ export default function SlideOverNotifications(props) { }} >
- -
- +
+
{[ { - icon: iconLoading.loading == true && iconLoading.icon =="RELOAD" ? :, + icon: + iconLoading.loading == true && iconLoading.icon == "RELOAD" ? ( + + ) : ( + + ), label: intl.formatMessage({ id: "notification.slideover.button.reload", }), @@ -170,26 +196,34 @@ export default function SlideOverNotifications(props) { setIconLoading({ icon: "RELOAD", loading: true }); await getNotifications(); setIconLoading({ icon: null, loading: false }); - }, }, { - icon: iconLoading.loading == true && iconLoading.icon =="NOTIFICATION" ? :, + icon: + iconLoading.loading == true && + iconLoading.icon == "NOTIFICATION" ? ( + + ) : ( + + ), label: intl.formatMessage({ id: "notification.slideover.button.subscribe", }), onClick: async () => { - - await subscribe() - - } + await subscribe(); + }, }, { - icon: iconLoading.loading == true && iconLoading.icon =="EMAIL" ? :, + icon: + iconLoading.loading == true && iconLoading.icon == "EMAIL" ? ( + + ) : ( + + ), label: intl.formatMessage({ id: "notification.slideover.button.markallasread", }), - onClick:async () => { + onClick: async () => { setIconLoading({ icon: "EMAIL", loading: true }); await markAllNotificationsAsRead(); setIconLoading({ icon: null, loading: false }); @@ -213,7 +247,6 @@ export default function SlideOverNotifications(props) { label={label} onClick={onClick} /> - ))}
diff --git a/frontend/src/components/notifications/Spinner.jsx b/frontend/src/components/notifications/Spinner.jsx index a9b11516fb..7f90d5f13c 100644 --- a/frontend/src/components/notifications/Spinner.jsx +++ b/frontend/src/components/notifications/Spinner.jsx @@ -1,18 +1,17 @@ -import React from 'react'; - +import React from "react"; + const Spinner = () => (
- +
); diff --git a/frontend/src/components/utils/Utils.js b/frontend/src/components/utils/Utils.js index c5612c93d0..e41480ca12 100644 --- a/frontend/src/components/utils/Utils.js +++ b/frontend/src/components/utils/Utils.js @@ -217,8 +217,6 @@ export const hasRole = (userSessionDetails, role) => { // this is complicated to enable it to format "smartly" as a person types // possible rework could allow it to only format completed numbers - - export const getFromOpenElisServerV2 = (url) => { return new Promise((resolve, reject) => { // Simulating the original callback-based function @@ -226,13 +224,12 @@ export const getFromOpenElisServerV2 = (url) => { if (res) { resolve(res); } else { - reject(new Error('Failed to fetch data')); + reject(new Error("Failed to fetch data")); } }); }); }; - export const convertAlphaNumLabNumForDisplay = (labNumber) => { if (!labNumber) { return labNumber; diff --git a/frontend/src/index.css b/frontend/src/index.css index af31e9c24a..026fe15cfc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -157,6 +157,3 @@ code { opacity: 0.5; } } - - - diff --git a/pom.xml b/pom.xml index b8e51bf0c0..61880cc82b 100644 --- a/pom.xml +++ b/pom.xml @@ -692,4 +692,4 @@ - \ No newline at end of file + diff --git a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java index 88c81429e7..13ecd873b8 100644 --- a/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java +++ b/src/main/java/org/openelisglobal/notifications/entity/NotificationSubscriptions.java @@ -90,14 +90,9 @@ public void setMessage(String message) { @Override public String toString() { - return "NotificationSubscription{" + - "userId=" + (user != null ? user.getId() : "null") + - ", pfEndpoint='" + pfEndpoint + '\'' + - ", pfP256dh='" + pfP256dh + '\'' + - ", pfAuth='" + pfAuth + '\'' + - ", user=" + (user != null ? user.toString() : "null") + - ", title='" + title + '\'' + - ", message='" + message + '\'' + - '}'; + return "NotificationSubscription{" + "userId=" + (user != null ? user.getId() : "null") + ", pfEndpoint='" + + pfEndpoint + '\'' + ", pfP256dh='" + pfP256dh + '\'' + ", pfAuth='" + pfAuth + '\'' + ", user=" + + (user != null ? user.toString() : "null") + ", title='" + title + '\'' + ", message='" + message + + '\'' + '}'; } } diff --git a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java index 392d014b1f..3480ebe5ed 100644 --- a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java +++ b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java @@ -1,12 +1,13 @@ package org.openelisglobal.notifications.rest; +import com.fasterxml.jackson.databind.ObjectMapper; import java.security.Security; import java.time.OffsetDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; - +import nl.martijndwars.webpush.PushService; import org.apache.http.HttpResponse; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.openelisglobal.login.valueholder.UserSessionData; @@ -27,9 +28,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import nl.martijndwars.webpush.PushService; - -import com.fasterxml.jackson.databind.ObjectMapper; @RequestMapping("/rest") @RestController @@ -83,10 +81,8 @@ public ResponseEntity saveNotification(@PathVariable String userId, @RequestB try { // Configure PushService with VAPID keys - PushService pushService = new PushService( - env.getProperty("vapid.public.key"), - env.getProperty("vapid.private.key"), - "mailto:your-email@example.com"); + PushService pushService = new PushService(env.getProperty("vapid.public.key"), + env.getProperty("vapid.private.key"), "mailto:your-email@example.com"); String title = "OpenELIS Global Notification"; String body = notification.getMessage(); @@ -103,10 +99,7 @@ public ResponseEntity saveNotification(@PathVariable String userId, @RequestB // Use fully qualified name for web push Notification nl.martijndwars.webpush.Notification webPushNotification = new nl.martijndwars.webpush.Notification( - ns.getPfEndpoint(), - ns.getPfP256dh(), - ns.getPfAuth(), - payloadJson); + ns.getPfEndpoint(), ns.getPfP256dh(), ns.getPfAuth(), payloadJson); HttpResponse response = pushService.send(webPushNotification); From e04492372d7104fd2a9ec93638771123e376bc8e Mon Sep 17 00:00:00 2001 From: vivek Date: Wed, 21 Aug 2024 10:11:06 +0530 Subject: [PATCH 07/11] change datatype --- frontend/public/service-worker.js | 2 +- .../notifications/SlideOverNotifications.jsx | 52 +++++++++++++++++-- frontend/src/languages/en.json | 3 +- frontend/src/languages/fr.json | 3 +- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js index effbde732a..4e5f5b3990 100644 --- a/frontend/public/service-worker.js +++ b/frontend/public/service-worker.js @@ -47,7 +47,7 @@ self.addEventListener("push", (event) => { event.waitUntil( self.registration.showNotification( - "OpenELIS Test Message Received", + "OpenELIS Message Received", notificationOptions, ), ); diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index 4656f1f782..725340a3ab 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -1,4 +1,10 @@ -import { Renew, NotificationFilled, Email, Filter } from "@carbon/icons-react"; +import { + Renew, + NotificationFilled, + Email, + Filter, + NotificationOff, +} from "@carbon/icons-react"; import { formatTimestamp, getFromOpenElisServer, @@ -7,7 +13,7 @@ import { } from "../utils/Utils"; import Spinner from "../common/Sprinner"; import { FormattedMessage, useIntl } from "react-intl"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export default function SlideOverNotifications(props) { const intl = useIntl(); @@ -16,6 +22,34 @@ export default function SlideOverNotifications(props) { loading: false, }); + const [subscriptionState, setSubscriptionState] = useState(null); + + const intialSubscriptionState = async () => { + try { + const res = await getFromOpenElisServerV2("/rest/notification/pnconfig"); + const reg = await navigator.serviceWorker.ready; + const subscription = await reg.pushManager.getSubscription(); + if (!subscription && !res?.pf_endpoint) { + setSubscriptionState("NotSubscribed"); + console.log("NotSubscribed"); + } else if (subscription?.endpoint === res?.pfEndpoint) { + setSubscriptionState("SubscribedOnThisDevice"); + console.log("SubscribedOnThisDevice"); + } else { + console.log("subscription?.endpoint", subscription?.endpoint); + + setSubscriptionState("SubscribedOnAnotherDevice"); + console.log("SubscribedOnAnotherDevice"); + } + } catch (error) { + console.error("Error checking subscription status:", error); + setIsSubscribed("Error"); + } + }; + + useEffect(() => { + intialSubscriptionState(); + }, []); async function subscribe() { try { // Set the loading state @@ -203,12 +237,20 @@ export default function SlideOverNotifications(props) { iconLoading.loading == true && iconLoading.icon == "NOTIFICATION" ? ( + ) : subscriptionState == "SubscribedOnThisDevice" ? ( + ) : ( ), - label: intl.formatMessage({ - id: "notification.slideover.button.subscribe", - }), + label: + subscriptionState && + subscriptionState == "SubscribedOnThisDevice" + ? intl.formatMessage({ + id: "notification.slideover.button.unsubscribe", + }) + : intl.formatMessage({ + id: "notification.slideover.button.subscribe", + }), onClick: async () => { await subscribe(); }, diff --git a/frontend/src/languages/en.json b/frontend/src/languages/en.json index a36765f8fc..d1d6912ff3 100644 --- a/frontend/src/languages/en.json +++ b/frontend/src/languages/en.json @@ -1234,5 +1234,6 @@ "label.test.batch.cancel.start": "You have selected to cancel the test", "label.test.batch.cancel.finish": "This is non-reversible.", "label.test.batch.replace.start": "The following", - "label.test.batch.no.change.finish": "tests will not be changed." + "label.test.batch.no.change.finish": "tests will not be changed.", + "notification.slideover.button.unsubscribe": "Unsubscribe" } diff --git a/frontend/src/languages/fr.json b/frontend/src/languages/fr.json index f15957610c..315e0efb71 100644 --- a/frontend/src/languages/fr.json +++ b/frontend/src/languages/fr.json @@ -1138,5 +1138,6 @@ "label.test.batch.cancel.start": "Vous avez choisi d'annuler le test", "label.test.batch.cancel.finish": "Cette action est irréversible.", "label.test.batch.replace.start": "Les tests suivants", - "label.test.batch.no.change.finish": "ne seront pas modifiés." + "label.test.batch.no.change.finish": "ne seront pas modifiés.", + "notification.slideover.button.unsubscribe": "Se désabonner" } From fdb9b822106b3fde519a5d5c92d51c1e9a0d2071 Mon Sep 17 00:00:00 2001 From: vivek Date: Wed, 21 Aug 2024 10:11:29 +0530 Subject: [PATCH 08/11] change datatype --- .../rest/NotificationRestController.java | 30 +++++++------------ .../2.8.x.x/notification_subscriptions.xml | 6 ++-- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java index 3480ebe5ed..0a95f6594d 100644 --- a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java +++ b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java @@ -112,25 +112,19 @@ public ResponseEntity saveNotification(@PathVariable String userId, @RequestB } - // @GetMapping("/notification/testpush") - // public ResponseEntity testPushNotification() { - - // // Fetch user and subscription - // SystemUser user = systemUserService.getUserById("111"); - // NotificationSubscriptions ns = - // notificationSubscriptionDAO.getNotificationSubscriptionByUserId(Long.valueOf(111)); + @GetMapping("/notification/pnconfig") + public ResponseEntity getSubscriptionDetails(HttpServletRequest request) { + String sysUserId = getSysUserId(request); + NotificationSubscriptions ns = notificationSubscriptionDAO + .getNotificationSubscriptionByUserId(Long.valueOf(sysUserId)); - // String title = "Test Notification"; - // String body = "This is a test notification"; - // String url = "https://example.com"; + if (ns == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Subscription not found"); + } - // if (ns.getPfAuth() == null || ns.getPfP256dh() == null || ns.getPfEndpoint() - // == null) { - // return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Subscription not - // found"); - // } + return ResponseEntity.ok().body(ns); - // } + } @PutMapping("/notification/markasread/{id}") public ResponseEntity markNotificationAsRead(@PathVariable String id) { @@ -179,10 +173,6 @@ public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notifi // Set the user entity directly notificationSubscription.setUser(user); - System.out.println("userId 1 " + sysUserId); - System.out.println("User ID 3 " + user.getId()); - System.out.println("User ID 4 from notification " + notificationSubscription.getUser().getId()); - notificationSubscriptionDAO.saveOrUpdate(notificationSubscription); return ResponseEntity.ok().body("Subscribed successfully"); diff --git a/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml b/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml index 77e8ca0790..c75c6ff2b1 100644 --- a/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml +++ b/src/main/resources/liquibase/2.8.x.x/notification_subscriptions.xml @@ -8,13 +8,13 @@ - + - + - + From 4c89db81989d5d89ca39f04a909c38edb4be4054 Mon Sep 17 00:00:00 2001 From: vivek Date: Wed, 21 Aug 2024 10:49:37 +0530 Subject: [PATCH 09/11] minor fix --- .../src/components/notifications/SlideOverNotifications.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index 725340a3ab..34a4417eb9 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -43,7 +43,8 @@ export default function SlideOverNotifications(props) { } } catch (error) { console.error("Error checking subscription status:", error); - setIsSubscribed("Error"); + setSubscriptionState("NotSubscribed") + } }; From e2bd68f1e26b761c932a652f4a28062a19bdd9c4 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 22 Aug 2024 12:49:40 +0530 Subject: [PATCH 10/11] add unsubscribe feature --- .../notifications/NoNotificationSVG.jsx | 294 +++++++++++++ .../notifications/SlideOverNotifications.jsx | 389 ++++-------------- frontend/src/components/utils/Utils.js | 14 + frontend/src/languages/en.json | 6 +- frontend/src/languages/fr.json | 6 +- .../dao/NotificationSubscriptionDAO.java | 2 + .../dao/NotificationSubscriptionDAOImpl.java | 15 + .../rest/NotificationRestController.java | 15 + 8 files changed, 425 insertions(+), 316 deletions(-) create mode 100644 frontend/src/components/notifications/NoNotificationSVG.jsx diff --git a/frontend/src/components/notifications/NoNotificationSVG.jsx b/frontend/src/components/notifications/NoNotificationSVG.jsx new file mode 100644 index 0000000000..978484be5f --- /dev/null +++ b/frontend/src/components/notifications/NoNotificationSVG.jsx @@ -0,0 +1,294 @@ +import { FormattedMessage } from "react-intl"; + +export default function NoNotificationSVG() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ +

+

+ +

+
+ ); +} diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index 34a4417eb9..5dbaef0c5e 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -7,16 +7,23 @@ import { } from "@carbon/icons-react"; import { formatTimestamp, - getFromOpenElisServer, getFromOpenElisServerV2, postToOpenElisServer, + putToOpenElisServer, + urlBase64ToUint8Array, + deleteToOpenElisServer, } from "../utils/Utils"; import Spinner from "../common/Sprinner"; -import { FormattedMessage, useIntl } from "react-intl"; -import { useEffect, useState } from "react"; +import { useIntl } from "react-intl"; +import { useContext, useEffect, useState } from "react"; +import { NotificationContext } from "../layout/Layout"; +import { AlertDialog } from "../common/CustomNotification"; +import NoNotificationSVG from "./NoNotificationSVG"; export default function SlideOverNotifications(props) { const intl = useIntl(); + const { notificationVisible, addNotification, setNotificationVisible } = + useContext(NotificationContext); const [iconLoading, setIconLoading] = useState({ icon: null, loading: false, @@ -24,6 +31,12 @@ export default function SlideOverNotifications(props) { const [subscriptionState, setSubscriptionState] = useState(null); + useEffect(() => { + // Whenever subscriptionState changes, re-check the subscription status + + intialSubscriptionState(); // Fetch the current subscription state again + }, [subscriptionState]); + const intialSubscriptionState = async () => { try { const res = await getFromOpenElisServerV2("/rest/notification/pnconfig"); @@ -43,14 +56,35 @@ export default function SlideOverNotifications(props) { } } catch (error) { console.error("Error checking subscription status:", error); - setSubscriptionState("NotSubscribed") - + setSubscriptionState("NotSubscribed"); } }; - useEffect(() => { - intialSubscriptionState(); - }, []); + async function unsubscribe() { + try { + putToOpenElisServer("/rest/notification/unsubscribe", null, (res) => { + addNotification({ + kind: "success", + message: intl.formatMessage({ + id: "notification.slideover.button.unsubscribe.success", + }), + title: intl.formatMessage({ id: "notification.title" }), + }); + setNotificationVisible(true); + setSubscriptionState("NotSubscribed"); + }); + } catch (e) { + addNotification({ + kind: "warning", + message: intl.formatMessage({ + id: "notification.slideover.button.unsubscribe.fail", + }), + title: intl.formatMessage({ id: "notification.title" }), + }); + setNotificationVisible(true); + } + } + async function subscribe() { try { // Set the loading state @@ -59,11 +93,13 @@ export default function SlideOverNotifications(props) { // Check if service workers are supported if (!("serviceWorker" in navigator)) { throw new Error("Service workers are not supported in this browser."); + return; } // Check if push messaging is supported if (!("PushManager" in window)) { throw new Error("Push messaging is not supported in this browser."); + return; } // Register the service worker if not already registered @@ -85,6 +121,7 @@ export default function SlideOverNotifications(props) { throw new Error( "Failed to retrieve public key from server: " + error.message, ); + return; }); // Convert the public key to a Uint8Array @@ -98,6 +135,7 @@ export default function SlideOverNotifications(props) { }) .catch((error) => { throw new Error("Push subscription failed: " + error.message); + return; }); // Encode the subscription keys @@ -115,8 +153,6 @@ export default function SlideOverNotifications(props) { pfAuth: auth, }; - console.log("data", data); - // Send the subscription data to the server postToOpenElisServer( "/rest/notification/subscribe", @@ -128,31 +164,38 @@ export default function SlideOverNotifications(props) { // Set the loading state to false setIconLoading({ icon: null, loading: false }); + addNotification({ + kind: "success", + message: intl.formatMessage({ + id: "notification.slideover.button.subscribe.success", + }), + title: intl.formatMessage({ id: "notification.title" }), + }); + setNotificationVisible(true); + setSubscriptionState("SubscribedOnThisDevice"); } catch (error) { // Handle any errors that occurred during the process console.error( "An error occurred during the subscription process:", error, ); - setIconLoading({ icon: null, loading: false }); - // Optionally set an error state here or provide user feedback - } - } - // Helper function to convert a URL-safe base64 string to a Uint8Array - function urlBase64ToUint8Array(base64String) { - const padding = "=".repeat((4 - (base64String.length % 4)) % 4); - const base64 = (base64String + padding) - .replace(/-/g, "+") - .replace(/_/g, "/"); + // let a = NotificationKinds. + + addNotification({ + kind: "warning", + message: intl.formatMessage({ + id: "notification.slideover.button.subscribe.fail", + }), + title: intl.formatMessage({ id: "notification.title" }), + }); + setNotificationVisible(true); + setSubscriptionState("NotSubscribed"); - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); + setIconLoading({ icon: null, loading: false }); - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); + // Optionally set an error state here or provide user feedback } - return outputArray; } const { @@ -206,6 +249,7 @@ export default function SlideOverNotifications(props) { margin: "0 auto", }} > + {notificationVisible === true ? : ""}

@@ -253,7 +297,11 @@ export default function SlideOverNotifications(props) { id: "notification.slideover.button.subscribe", }), onClick: async () => { - await subscribe(); + if (subscriptionState == "SubscribedOnThisDevice") { + unsubscribe(); + } else { + subscribe(); + } }, }, { @@ -344,294 +392,7 @@ export default function SlideOverNotifications(props) {
)) ) : ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- -

-

- -

-
+ )}
diff --git a/frontend/src/components/utils/Utils.js b/frontend/src/components/utils/Utils.js index e41480ca12..e69b63cee2 100644 --- a/frontend/src/components/utils/Utils.js +++ b/frontend/src/components/utils/Utils.js @@ -342,3 +342,17 @@ export function formatTimestamp(timestamp) { // Combine and return the formatted string return `${formattedHours}:${formattedMinutes} ${ampm}; ${formattedDay}/${formattedMonth}/${year}`; } + +// Helper function to convert a URL-safe base64 string to a Uint8Array +export function urlBase64ToUint8Array(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; +} diff --git a/frontend/src/languages/en.json b/frontend/src/languages/en.json index d1d6912ff3..d595c89fee 100644 --- a/frontend/src/languages/en.json +++ b/frontend/src/languages/en.json @@ -1235,5 +1235,9 @@ "label.test.batch.cancel.finish": "This is non-reversible.", "label.test.batch.replace.start": "The following", "label.test.batch.no.change.finish": "tests will not be changed.", - "notification.slideover.button.unsubscribe": "Unsubscribe" + "notification.slideover.button.unsubscribe": "Unsubscribe", + "notification.slideover.button.unsubscribe.success": "Unsubscribed successfully", + "notification.slideover.button.unsubscribe.fail": "Unsubscribe failed", + "notification.slideover.button.subscribe.success": "Subscribed successfully", + "notification.slideover.button.subscribe.fail": "Subscribe failed" } diff --git a/frontend/src/languages/fr.json b/frontend/src/languages/fr.json index 315e0efb71..6ae9c22038 100644 --- a/frontend/src/languages/fr.json +++ b/frontend/src/languages/fr.json @@ -1139,5 +1139,9 @@ "label.test.batch.cancel.finish": "Cette action est irréversible.", "label.test.batch.replace.start": "Les tests suivants", "label.test.batch.no.change.finish": "ne seront pas modifiés.", - "notification.slideover.button.unsubscribe": "Se désabonner" + "notification.slideover.button.unsubscribe": "Se désabonner", + "notification.slideover.button.unsubscribe.success": "Désabonnement réussi", + "notification.slideover.button.unsubscribe.fail": "Échec de l'annulation de l'abonnement", + "notification.slideover.button.subscribe.success": "Abonnement réussi", + "notification.slideover.button.subscribe.fail": "Échec de l'abonnement" } diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java index f536e35368..2727a10cc1 100644 --- a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAO.java @@ -12,4 +12,6 @@ public interface NotificationSubscriptionDAO { void saveOrUpdate(NotificationSubscriptions notificationSubscription); + void delete(NotificationSubscriptions ns); + } \ No newline at end of file diff --git a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java index a38c0d1a07..9e630a8d6b 100644 --- a/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java +++ b/src/main/java/org/openelisglobal/notifications/dao/NotificationSubscriptionDAOImpl.java @@ -36,7 +36,22 @@ public void updateNotificationSubscription(NotificationSubscriptions notificatio entityManager.merge(notificationSubscription); } + @Override + @Transactional + public void delete(NotificationSubscriptions ns) { + // Reattach the entity to the current session + NotificationSubscriptions attachedEntity = entityManager.find(NotificationSubscriptions.class, ns.getId()); + + if (attachedEntity != null) { + // Now remove the attached entity + entityManager.remove(attachedEntity); + } else { + throw new IllegalArgumentException("Subscription not found or already deleted"); + } + } + // Update saveOrUpdate method + @Transactional public void saveOrUpdate(NotificationSubscriptions notificationSubscription) { diff --git a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java index 0a95f6594d..a45f3f2a32 100644 --- a/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java +++ b/src/main/java/org/openelisglobal/notifications/rest/NotificationRestController.java @@ -178,6 +178,21 @@ public ResponseEntity subscribe(@RequestBody NotificationSubscriptions notifi return ResponseEntity.ok().body("Subscribed successfully"); } + @PutMapping("/notification/unsubscribe") + public ResponseEntity unsubscribe(HttpServletRequest request) { + String sysUserId = getSysUserId(request); + NotificationSubscriptions ns = notificationSubscriptionDAO + .getNotificationSubscriptionByUserId(Long.valueOf(sysUserId)); + + if (ns == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Subscription not found"); + } + + notificationSubscriptionDAO.delete(ns); + + return ResponseEntity.ok().body("Unsubscribed successfully"); + } + protected String getSysUserId(HttpServletRequest request) { UserSessionData usd = (UserSessionData) request.getSession().getAttribute(USER_SESSION_DATA); if (usd == null) { From 5ae9898a8c1cc0104ceddf8b31c75afc15cd7129 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 22 Aug 2024 13:08:46 +0530 Subject: [PATCH 11/11] minor fix --- .../src/components/notifications/SlideOverNotifications.jsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/components/notifications/SlideOverNotifications.jsx b/frontend/src/components/notifications/SlideOverNotifications.jsx index 5dbaef0c5e..a6aab35b47 100644 --- a/frontend/src/components/notifications/SlideOverNotifications.jsx +++ b/frontend/src/components/notifications/SlideOverNotifications.jsx @@ -93,13 +93,11 @@ export default function SlideOverNotifications(props) { // Check if service workers are supported if (!("serviceWorker" in navigator)) { throw new Error("Service workers are not supported in this browser."); - return; } // Check if push messaging is supported if (!("PushManager" in window)) { throw new Error("Push messaging is not supported in this browser."); - return; } // Register the service worker if not already registered @@ -121,7 +119,6 @@ export default function SlideOverNotifications(props) { throw new Error( "Failed to retrieve public key from server: " + error.message, ); - return; }); // Convert the public key to a Uint8Array @@ -135,7 +132,6 @@ export default function SlideOverNotifications(props) { }) .catch((error) => { throw new Error("Push subscription failed: " + error.message); - return; }); // Encode the subscription keys