|
|
@@ -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);
|