Use the same notification pipeline everywhere
This means less duplication and `actions` support for all notifications.
This commit is contained in:
parent
fa418eef16
commit
b197ea3ab6
5 changed files with 102 additions and 73 deletions
|
@ -1,39 +1,34 @@
|
|||
import { openUrl, playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
||||
import { formatMessage, formatTitleWithDefault } from "./notificationUtils";
|
||||
import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
||||
import { getNotificationParams } from "./notificationUtils";
|
||||
import prefs from "./Prefs";
|
||||
import logo from "../img/ntfy.png";
|
||||
import routes from "../components/routes";
|
||||
|
||||
/**
|
||||
* The notifier is responsible for displaying desktop notifications. Note that not all modern browsers
|
||||
* support this; most importantly, all iOS browsers do not support window.Notification.
|
||||
*/
|
||||
class Notifier {
|
||||
async notify(subscription, notification, onClickFallback) {
|
||||
async notify(subscription, notification) {
|
||||
if (!this.supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||
const displayName = topicDisplayName(subscription);
|
||||
const message = formatMessage(notification);
|
||||
const title = formatTitleWithDefault(notification, displayName);
|
||||
const image = notification.attachment?.name.match(/\.(png|jpe?g|gif|webp)$/i) ? notification.attachment.url : undefined;
|
||||
await this.playSound();
|
||||
|
||||
// Show notification
|
||||
console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);
|
||||
// Please update sw.js if formatting changes
|
||||
const n = new Notification(title, {
|
||||
body: message,
|
||||
tag: subscription.id,
|
||||
icon: image ?? logo,
|
||||
image,
|
||||
timestamp: message.time * 1_000,
|
||||
});
|
||||
if (notification.click) {
|
||||
n.onclick = () => openUrl(notification.click);
|
||||
} else {
|
||||
n.onclick = () => onClickFallback(subscription);
|
||||
}
|
||||
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||
const defaultTitle = topicDisplayName(subscription);
|
||||
|
||||
console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}`);
|
||||
|
||||
const registration = await this.serviceWorkerRegistration();
|
||||
await registration.showNotification(
|
||||
...getNotificationParams({
|
||||
subscriptionId: subscription.id,
|
||||
message: notification,
|
||||
defaultTitle,
|
||||
topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async playSound() {
|
||||
|
@ -73,11 +68,15 @@ class Notifier {
|
|||
}
|
||||
|
||||
async pushManager() {
|
||||
return (await this.serviceWorkerRegistration()).pushManager;
|
||||
}
|
||||
|
||||
async serviceWorkerRegistration() {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (!registration) {
|
||||
throw new Error("No service worker registration found");
|
||||
}
|
||||
return registration.pushManager;
|
||||
return registration;
|
||||
}
|
||||
|
||||
notRequested() {
|
||||
|
|
|
@ -42,7 +42,7 @@ class SubscriptionManager {
|
|||
return this.db.subscriptions.get(subscriptionId);
|
||||
}
|
||||
|
||||
async notify(subscriptionId, notification, defaultClickAction) {
|
||||
async notify(subscriptionId, notification) {
|
||||
const subscription = await this.get(subscriptionId);
|
||||
if (subscription.mutedUntil > 0) {
|
||||
return;
|
||||
|
@ -53,7 +53,7 @@ class SubscriptionManager {
|
|||
return;
|
||||
}
|
||||
|
||||
await Promise.all([notifier.playSound(), notifier.notify(subscription, notification, defaultClickAction)]);
|
||||
await notifier.notify(subscription, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,7 +16,7 @@ export const formatTitle = (m) => {
|
|||
return m.title;
|
||||
};
|
||||
|
||||
export const formatTitleWithDefault = (m, fallback) => {
|
||||
const formatTitleWithDefault = (m, fallback) => {
|
||||
if (m.title) {
|
||||
return formatTitle(m);
|
||||
}
|
||||
|
@ -33,3 +33,38 @@ export const formatMessage = (m) => {
|
|||
}
|
||||
return m.message;
|
||||
};
|
||||
|
||||
const isImage = (filenameOrUrl) => filenameOrUrl?.match(/\.(png|jpe?g|gif|webp)$/i) ?? false;
|
||||
|
||||
export const icon = "/static/images/ntfy.png";
|
||||
export const badge = "/static/images/mask-icon.svg";
|
||||
|
||||
export const getNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => {
|
||||
const image = isImage(message.attachment?.name) ? message.attachment.url : undefined;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
||||
return [
|
||||
formatTitleWithDefault(message, defaultTitle),
|
||||
{
|
||||
body: formatMessage(message),
|
||||
badge,
|
||||
icon,
|
||||
image,
|
||||
timestamp: message.time * 1_000,
|
||||
tag: subscriptionId,
|
||||
renotify: true,
|
||||
silent: false,
|
||||
// This is used by the notification onclick event
|
||||
data: {
|
||||
message,
|
||||
topicRoute,
|
||||
},
|
||||
actions: message.actions
|
||||
?.filter(({ action }) => action === "view" || action === "http")
|
||||
.map(({ label }) => ({
|
||||
action: label,
|
||||
title: label,
|
||||
})),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue