From d05ba52f87ac2d68212d53c0cbab8599d401ce73 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:42:47 +0100 Subject: [PATCH 1/5] Add native browser notification helper functions --- .../Frontend/js/src/BrowserNotification.js | 93 +++++++++++++++++++ .../components/CopyToClipboardComponent.js | 2 + Framework/Frontend/js/src/index.js | 3 + .../js/src/utilities/browserContext.js | 22 +++++ 4 files changed, 120 insertions(+) create mode 100644 Framework/Frontend/js/src/BrowserNotification.js create mode 100644 Framework/Frontend/js/src/utilities/browserContext.js diff --git a/Framework/Frontend/js/src/BrowserNotification.js b/Framework/Frontend/js/src/BrowserNotification.js new file mode 100644 index 000000000..65dfb3dfb --- /dev/null +++ b/Framework/Frontend/js/src/BrowserNotification.js @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/* Global: window */ + +import { isContextSecure } from './utilities/browserContext.js'; + +/** + * Get the current browser notification permission. + * @returns {NotificationPermission|undefined} {@link NotificationPermission}, or `undefined` if unsupported. + */ +export const getBrowserNotificationPermission = () => + window?.Notification?.permission; + +/** + * Request browser notification permission (async/await). + * @returns {Promise} Resolves to {@link NotificationPermission}, or `undefined` if unsupported + */ +export const requestBrowserNotificationPermissions = async () => { + if (!isContextSecure()) { + return undefined; + } + + const permission = getBrowserNotificationPermission(); + if (permission === 'granted') { + return permission; + } + + return await window.Notification?.requestPermission(); +}; + +/** + * Check if notifications can be shown immediately. + * @returns {boolean} `true` if the Notification API is available, the context is secure + * and the {@link NotificationPermission browser notification permission} is `granted`, `false` otherwise. + */ +export const checkBrowserNotificationPermissions = () => + isContextSecure() && getBrowserNotificationPermission() === 'granted'; + +/** + * @typedef {object} NotificationOptions + * @property {string} title Notification title + * @property {string} [body] Notification body + * @property {string} [icon] Notification icon URL (defaults to server icon) + * @property {(event: Event) => void} [onclick] Triggered when the notification is clicked + * @property {(event: Event) => void} [onerror] Triggered if the notification fails to display + * @property {(event: Event) => void} [onshow] Triggered when the notification is shown + * @property {(event: Event) => void} [onclose] Triggered when the notification is closed + */ + +/** + * Show a native browser notification. + * @param {NotificationOptions} options - The browser notification options + * @returns {Notification|null} {@link Notification} instance, or `null` if unavailable + */ +export const showNativeBrowserNotification = (options) => { + if (!checkBrowserNotificationPermissions()) { + return null; + } + + const { + title, + onclick, + onerror, + onshow, + onclose, + icon = '/favicon.ico', + ...notificationOptions + } = options; + if (!title) { + return null; + } + + const notification = new window.Notification(title, { icon, ...notificationOptions }); + Object.entries({ onclick, onerror, onshow, onclose }) + .filter(([_, eventFunc]) => typeof eventFunc === 'function') + .forEach(([eventName, eventFunc]) => { + notification[eventName] = eventFunc; + }); + + return notification; +}; diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index f2207d194..0754b2b09 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -65,6 +65,8 @@ export class CopyToClipboardComponent extends StatefulComponent { /** * Checks if context is secure (HTTPS) * + * @deprecated Use `isContextSecure` from `./utilities/browserContext.js` instead. + * Calling this method directly is discouraged; it may be removed in future versions. * @returns {boolean} Returns `true` if context is secure */ isContextSecure() { diff --git a/Framework/Frontend/js/src/index.js b/Framework/Frontend/js/src/index.js index 3541c843e..c9dea8ef8 100644 --- a/Framework/Frontend/js/src/index.js +++ b/Framework/Frontend/js/src/index.js @@ -26,6 +26,7 @@ export { default as switchCase } from './switchCase.js'; export { documentClickTaggedEventRegistry } from './utilities/documentClickTaggedEventRegistry.js'; export { buildUrl } from './utilities/buildUrl.js'; export { parseUrlParameters } from './utilities/parseUrlParameters.js'; +export { isContextSecure } from './utilities/browserContext.js'; // Formatters export { formatTimeDuration } from './formatter/formatTimeDuration.js'; @@ -54,3 +55,5 @@ export * from './icons.js'; export * from './chart.js'; export * from './Notification.js'; + +export * from './BrowserNotification.js'; diff --git a/Framework/Frontend/js/src/utilities/browserContext.js b/Framework/Frontend/js/src/utilities/browserContext.js new file mode 100644 index 000000000..9b73dbd15 --- /dev/null +++ b/Framework/Frontend/js/src/utilities/browserContext.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/* Global: window */ + +/** + * Checks whether the context is secure (HTTPS) + * @returns {boolean} Returns `true` if context is secure, `false` otherwise. + */ +export const isContextSecure = () => + window.isSecureContext; From b13cd140768cf77e255b3e52262811742f6efa5a Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:55:12 +0100 Subject: [PATCH 2/5] Use `isContextSecure` from `browserContext.js` in `CopyToClipboardComponent.js` --- .../js/src/components/CopyToClipboardComponent.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index 0754b2b09..ccc4c898a 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -14,6 +14,7 @@ import { StatefulComponent } from './StatefulComponent.js'; import { iconCheck, iconLinkIntact } from '../icons.js'; import { h } from '../renderer.js'; +import { isContextSecure } from '../utilities/browserContext.js'; /** * Represents a component that allows copying text to the clipboard. @@ -49,7 +50,7 @@ export class CopyToClipboardComponent extends StatefulComponent { * @returns {void} */ checkClipboardAvailability() { - if (!this.isContextSecure()) { + if (!isContextSecure()) { throw new Error('Clipboard not available in a non-secure context.'); } @@ -62,17 +63,6 @@ export class CopyToClipboardComponent extends StatefulComponent { } } - /** - * Checks if context is secure (HTTPS) - * - * @deprecated Use `isContextSecure` from `./utilities/browserContext.js` instead. - * Calling this method directly is discouraged; it may be removed in future versions. - * @returns {boolean} Returns `true` if context is secure - */ - isContextSecure() { - return window.isSecureContext; - } - /** * Checks if the clipboard API is available in the user's browser. * From 8b3447ecc54a77393cabd7c5ed79fa075a30b422 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:58:50 +0100 Subject: [PATCH 3/5] Renamed `BrowserNotification.js` to `browserNotification.js` and move it to the `utilities` directory --- Framework/Frontend/js/src/index.js | 2 +- .../browserNotification.js} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Framework/Frontend/js/src/{BrowserNotification.js => utilities/browserNotification.js} (97%) diff --git a/Framework/Frontend/js/src/index.js b/Framework/Frontend/js/src/index.js index c9dea8ef8..27391cf72 100644 --- a/Framework/Frontend/js/src/index.js +++ b/Framework/Frontend/js/src/index.js @@ -56,4 +56,4 @@ export * from './icons.js'; export * from './chart.js'; export * from './Notification.js'; -export * from './BrowserNotification.js'; +export * from './utilities/browserNotification.js'; diff --git a/Framework/Frontend/js/src/BrowserNotification.js b/Framework/Frontend/js/src/utilities/browserNotification.js similarity index 97% rename from Framework/Frontend/js/src/BrowserNotification.js rename to Framework/Frontend/js/src/utilities/browserNotification.js index 65dfb3dfb..b4a16b697 100644 --- a/Framework/Frontend/js/src/BrowserNotification.js +++ b/Framework/Frontend/js/src/utilities/browserNotification.js @@ -14,7 +14,7 @@ /* Global: window */ -import { isContextSecure } from './utilities/browserContext.js'; +import { isContextSecure } from './browserContext.js'; /** * Get the current browser notification permission. From e67dbdcded24de2f618ab51dfab9d87061593f51 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:11:01 +0100 Subject: [PATCH 4/5] Add enum `BrowserNotificationPermission` to `browserNotification.js` and update docs --- .../js/src/utilities/browserNotification.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Framework/Frontend/js/src/utilities/browserNotification.js b/Framework/Frontend/js/src/utilities/browserNotification.js index b4a16b697..7d845f4f6 100644 --- a/Framework/Frontend/js/src/utilities/browserNotification.js +++ b/Framework/Frontend/js/src/utilities/browserNotification.js @@ -16,16 +16,27 @@ import { isContextSecure } from './browserContext.js'; +/** + * Browser notification permission values. + * Mirrors the Notification API specification. + * @link https://developer.mozilla.org/en-US/docs/Web/API/Notification/permission_static + */ +export const BrowserNotificationPermission = Object.freeze({ + GRANTED: 'granted', + DEFAULT: 'default', + DENIED: 'denied', +}); + /** * Get the current browser notification permission. - * @returns {NotificationPermission|undefined} {@link NotificationPermission}, or `undefined` if unsupported. + * @returns {BrowserNotificationPermission|undefined} One of {@link BrowserNotificationPermission}, or `undefined` if unsupported. */ export const getBrowserNotificationPermission = () => window?.Notification?.permission; /** * Request browser notification permission (async/await). - * @returns {Promise} Resolves to {@link NotificationPermission}, or `undefined` if unsupported + * @returns {Promise} One of {@link BrowserNotificationPermission}, or `undefined` if unsupported */ export const requestBrowserNotificationPermissions = async () => { if (!isContextSecure()) { @@ -33,7 +44,7 @@ export const requestBrowserNotificationPermissions = async () => { } const permission = getBrowserNotificationPermission(); - if (permission === 'granted') { + if (permission === BrowserNotificationPermission.GRANTED) { return permission; } @@ -43,10 +54,10 @@ export const requestBrowserNotificationPermissions = async () => { /** * Check if notifications can be shown immediately. * @returns {boolean} `true` if the Notification API is available, the context is secure - * and the {@link NotificationPermission browser notification permission} is `granted`, `false` otherwise. + * and the current {@link BrowserNotificationPermission} is {@link BrowserNotificationPermission.GRANTED}, `false` otherwise. */ export const checkBrowserNotificationPermissions = () => - isContextSecure() && getBrowserNotificationPermission() === 'granted'; + isContextSecure() && getBrowserNotificationPermission() === BrowserNotificationPermission.GRANTED; /** * @typedef {object} NotificationOptions From e352565121a312b16c08a1b98dfdcbe420e07d5b Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:21:32 +0100 Subject: [PATCH 5/5] Rename `checkBrowserNotificationPermissions` to `areBrowserNotificationsGranted` in `browserNotification.js` --- Framework/Frontend/js/src/utilities/browserNotification.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/Frontend/js/src/utilities/browserNotification.js b/Framework/Frontend/js/src/utilities/browserNotification.js index 7d845f4f6..293e608b6 100644 --- a/Framework/Frontend/js/src/utilities/browserNotification.js +++ b/Framework/Frontend/js/src/utilities/browserNotification.js @@ -56,7 +56,7 @@ export const requestBrowserNotificationPermissions = async () => { * @returns {boolean} `true` if the Notification API is available, the context is secure * and the current {@link BrowserNotificationPermission} is {@link BrowserNotificationPermission.GRANTED}, `false` otherwise. */ -export const checkBrowserNotificationPermissions = () => +export const areBrowserNotificationsGranted = () => isContextSecure() && getBrowserNotificationPermission() === BrowserNotificationPermission.GRANTED; /** @@ -76,7 +76,7 @@ export const checkBrowserNotificationPermissions = () => * @returns {Notification|null} {@link Notification} instance, or `null` if unavailable */ export const showNativeBrowserNotification = (options) => { - if (!checkBrowserNotificationPermissions()) { + if (!areBrowserNotificationsGranted()) { return null; }