Web: Show red notification dot on favicon when there are unread messages

This commit is contained in:
binwiederhier
2026-02-08 10:28:46 -05:00
parent 623fd4f224
commit fe6ee1efa0
3 changed files with 84 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ import pop from "../sounds/pop.mp3";
import popSwoosh from "../sounds/pop-swoosh.mp3";
import config from "./config";
import emojisMapped from "./emojisMapped";
import { THEME } from "./Prefs";
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
@@ -274,6 +275,84 @@ export const urlB64ToUint8Array = (base64String) => {
return outputArray;
};
export const darkModeEnabled = (prefersDarkMode, themePreference) => {
switch (themePreference) {
case THEME.DARK:
return true;
case THEME.LIGHT:
return false;
case THEME.SYSTEM:
default:
return prefersDarkMode;
}
};
// Canvas-based favicon with a red notification dot when there are unread messages
let faviconCanvas;
let faviconOriginalIcon;
const loadFaviconIcon = () =>
new Promise((resolve) => {
if (faviconOriginalIcon) {
resolve(faviconOriginalIcon);
return;
}
const img = new Image();
img.onload = () => {
faviconOriginalIcon = img;
resolve(img);
};
img.onerror = () => resolve(null);
// Use PNG instead of ICO — .ico files can't be reliably drawn to canvas in all browsers
img.src = "/static/images/ntfy.png";
});
export const updateFavicon = async (count) => {
const size = 32;
const img = await loadFaviconIcon();
if (!img) {
return;
}
if (!faviconCanvas) {
faviconCanvas = document.createElement("canvas");
faviconCanvas.width = size;
faviconCanvas.height = size;
}
const ctx = faviconCanvas.getContext("2d");
ctx.clearRect(0, 0, size, size);
ctx.drawImage(img, 0, 0, size, size);
if (count > 0) {
const dotRadius = 5;
const borderWidth = 2;
const dotX = size - dotRadius - borderWidth + 1;
const dotY = size - dotRadius - borderWidth + 1;
// Transparent border: erase a ring around the dot so the icon doesn't bleed into it
ctx.save();
ctx.globalCompositeOperation = "destination-out";
ctx.beginPath();
ctx.arc(dotX, dotY, dotRadius + borderWidth, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
// Red dot
ctx.beginPath();
ctx.arc(dotX, dotY, dotRadius, 0, 2 * Math.PI);
ctx.fillStyle = "#dc3545";
ctx.fill();
}
const link = document.querySelector("link[rel='icon']");
if (link) {
link.href = faviconCanvas.toDataURL("image/png");
}
};
export const copyToClipboard = (text) => {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);