Files
MindSift/frontend/src/lib/push.ts
T
sandrodachandClaude Opus 5.5 38b8c92899
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m15s
feat(notify): web push as a notification channel
- VAPID keys from VAPID_* or generated once (push_keys, migration 9);
  devices in push_subscriptions, forgotten on 404/410 from the push service
- push column for every event (also due tasks) and the daily summary,
  bundles as one notification; /push/key, /devices, /subscribe,
  /unsubscribe, /test
- settings: switch push on per device, test it, see the devices; the
  service worker shows notifications and focuses MindSift on click
- tests decrypt a sent push like a browser (RFC 8291) and check VAPID

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:21:55 +01:00

77 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { api } from './api';
// Web push in this browser: the service worker shows the notifications, the server sends them over the
// browser's push service (VAPID). iPhones and iPads need MindSift added to the home screen first.
export function pushSupported(): boolean {
return typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
}
/** iOS/iPadOS outside the installed home screen app: push is not offered there */
export function needsHomeScreenInstall(): boolean {
if (typeof navigator === 'undefined') return false;
const ios = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const standalone =
window.matchMedia?.('(display-mode: standalone)').matches || (navigator as Navigator & { standalone?: boolean }).standalone === true;
return ios && !standalone;
}
export function deviceName(ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''): string {
const browser = /Edg\//.test(ua) ? 'Edge' : /Firefox\//.test(ua) ? 'Firefox' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : 'Browser';
const os = /Android/.test(ua) ? 'Android' : /iPhone|iPad|iPod/.test(ua) ? 'iOS' : /Windows/.test(ua) ? 'Windows' : /Mac OS X/.test(ua) ? 'macOS' : /Linux/.test(ua) ? 'Linux' : '';
return os ? `${browser} auf ${os}` : browser;
}
export function urlBase64ToUint8Array(base64: string): Uint8Array {
const padded = (base64 + '='.repeat((4 - (base64.length % 4)) % 4)).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(padded);
const out = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
return out;
}
async function registration(): Promise<ServiceWorkerRegistration> {
return navigator.serviceWorker.ready;
}
/** The subscription of this browser, if push is switched on here. */
export async function currentPushSubscription(): Promise<PushSubscription | null> {
if (!pushSupported()) return null;
try {
return await (await registration()).pushManager.getSubscription();
} catch {
return null;
}
}
export async function enablePush(): Promise<void> {
if (!pushSupported()) throw new Error('Dieser Browser unterstützt keine Push-Benachrichtigungen');
if (needsHomeScreenInstall()) throw new Error('Auf iPhone und iPad zuerst MindSift „Zum Home-Bildschirm“ hinzufügen und von dort öffnen');
const key = await api.getPushKey();
if (!key.enabled || !key.publicKey) throw new Error('Push ist auf diesem Server nicht verfügbar');
const permission = await Notification.requestPermission();
if (permission !== 'granted') throw new Error('Benachrichtigungen wurden im Browser nicht erlaubt');
const reg = await registration();
let sub = await reg.pushManager.getSubscription();
if (!sub) {
try {
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(key.publicKey) as BufferSource,
});
} catch {
// e.g. "Registration failed – permission denied": blocked for the site or no push service reachable
throw new Error('Der Browser hat die Anmeldung beim Push-Dienst abgelehnt (Berechtigung blockiert oder Push-Dienst nicht erreichbar)');
}
}
await api.subscribePush(sub.toJSON(), deviceName());
}
export async function disablePush(): Promise<void> {
const sub = await currentPushSubscription();
if (!sub) return;
const endpoint = sub.endpoint;
await sub.unsubscribe().catch(() => false);
await api.unsubscribePush(endpoint).catch(() => {});
}