mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement notifications for Electron
This commit is contained in:
Vendored
+12
-1
@@ -1,8 +1,19 @@
|
|||||||
export type Platform = "win32" | "darwin" | "linux"
|
export type Platform = "win32" | "darwin" | "linux"
|
||||||
|
|
||||||
|
export interface ElectronNotifications {
|
||||||
|
requestPermission: () => Promise<NotificationPermission>;
|
||||||
|
show: (options: {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
icon?: string;
|
||||||
|
tag?: string;
|
||||||
|
}) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ElectronInterface {
|
export interface ElectronInterface {
|
||||||
desktop: true,
|
desktop: true,
|
||||||
platform: Platform
|
platform: Platform,
|
||||||
|
notifications: ElectronNotifications
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow, Notification, ipcMain } from 'electron';
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
const win = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
title: 'Main window',
|
title: 'Main window',
|
||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
minHeight: 420,
|
minHeight: 420,
|
||||||
@@ -19,8 +21,45 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.VITE_DEV_SERVER_URL) {
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(process.env.VITE_DEV_SERVER_URL);
|
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||||
} else {
|
} else {
|
||||||
win.loadFile('frontend/build/electron/dist/index.html');
|
mainWindow.loadFile('frontend/build/electron/dist/index.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle notification permission requests
|
||||||
|
ipcMain.handle('request-notification-permission', async () => {
|
||||||
|
if (Notification.isSupported()) {
|
||||||
|
return 'granted';
|
||||||
|
}
|
||||||
|
return 'denied';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle showing notifications
|
||||||
|
ipcMain.handle('show-notification', async (event, options) => {
|
||||||
|
if (Notification.isSupported()) {
|
||||||
|
try {
|
||||||
|
const notification = new Notification({
|
||||||
|
title: options.title,
|
||||||
|
body: options.body,
|
||||||
|
icon: options.icon,
|
||||||
|
silent: false,
|
||||||
|
urgency: 'normal'
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.on('click', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.show();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating notification:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import { contextBridge } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import type { ElectronInterface, Platform } from "../electron";
|
import type { ElectronInterface, Platform } from "../electron";
|
||||||
|
|
||||||
const electronInterface: ElectronInterface = {
|
const electronInterface: ElectronInterface = {
|
||||||
desktop: true,
|
desktop: true,
|
||||||
platform: process.platform as Platform
|
platform: process.platform as Platform,
|
||||||
|
notifications: {
|
||||||
|
requestPermission: () => ipcRenderer.invoke('request-notification-permission'),
|
||||||
|
show: (options: any) => ipcRenderer.invoke('show-notification', options)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronInterface", electronInterface);
|
contextBridge.exposeInMainWorld("electronInterface", electronInterface);
|
||||||
@@ -2,7 +2,8 @@ import { useState, useEffect } from "react";
|
|||||||
import { PRODUCT_NAME, API_BASE_URL } from "../../../core/config";
|
import { PRODUCT_NAME, API_BASE_URL } from "../../../core/config";
|
||||||
import type { DialogProps } from "../../../core/types";
|
import type { DialogProps } from "../../../core/types";
|
||||||
import { MaterialDialog } from "../core/Dialog";
|
import { MaterialDialog } from "../core/Dialog";
|
||||||
import { pushNotificationManager } from "../../../utils/pushNotifications";
|
import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../utils/notifications";
|
||||||
|
import { isElectron } from "../../../electron/electron";
|
||||||
import { useAppState } from "../../state";
|
import { useAppState } from "../../state";
|
||||||
import type { Switch } from "mdui/components/switch";
|
import type { Switch } from "mdui/components/switch";
|
||||||
import { getAuthHeaders } from "../../../auth/api";
|
import { getAuthHeaders } from "../../../auth/api";
|
||||||
@@ -14,8 +15,10 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
|||||||
const user = useAppState(state => state.user);
|
const user = useAppState(state => state.user);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPushSupported(pushNotificationManager.isSupported());
|
setPushSupported(isSupported());
|
||||||
setPushNotificationsEnabled(!!pushNotificationManager.getSubscription());
|
// For Electron, we assume notifications are enabled if supported
|
||||||
|
// For web browsers, we check if there's a subscription
|
||||||
|
setPushNotificationsEnabled(isSupported());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePanelChange = (panelId: string) => {
|
const handlePanelChange = (panelId: string) => {
|
||||||
@@ -27,19 +30,26 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
await pushNotificationManager.initialize();
|
const initialized = await initialize();
|
||||||
const permission = await pushNotificationManager.requestPermission();
|
if (initialized) {
|
||||||
|
await subscribe(user.authToken);
|
||||||
|
|
||||||
|
// For Electron, start the notification receiver
|
||||||
|
if (isElectron) {
|
||||||
|
await startElectronReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
if (permission === "granted") {
|
|
||||||
const subscription = await pushNotificationManager.subscribe();
|
|
||||||
if (subscription) {
|
|
||||||
await pushNotificationManager.sendSubscriptionToServer(user.authToken);
|
|
||||||
setPushNotificationsEnabled(true);
|
setPushNotificationsEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await pushNotificationManager.unsubscribe();
|
await unsubscribe();
|
||||||
// Call API to unsubscribe on server
|
|
||||||
|
// For Electron, stop the notification receiver
|
||||||
|
if (isElectron) {
|
||||||
|
stopElectronReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call API to unsubscribe on server (for web browsers)
|
||||||
await fetch(`${API_BASE_URL}/push/unsubscribe`, {
|
await fetch(`${API_BASE_URL}/push/unsubscribe`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: getAuthHeaders(user.authToken)
|
headers: getAuthHeaders(user.authToken)
|
||||||
@@ -47,7 +57,7 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
|||||||
setPushNotificationsEnabled(false);
|
setPushNotificationsEnabled(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to toggle push notifications:", error);
|
console.error("Failed to toggle notifications:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { useRef } from "react";
|
|||||||
import type { TextField } from "mdui/components/text-field";
|
import type { TextField } from "mdui/components/text-field";
|
||||||
import { useAppState } from "../state";
|
import { useAppState } from "../state";
|
||||||
import { MaterialTextField } from "../components/core/TextField";
|
import { MaterialTextField } from "../components/core/TextField";
|
||||||
import { pushNotificationManager } from "../../utils/pushNotifications";
|
import { initialize, isSupported, startElectronReceiver, subscribe } from "../../utils/notifications";
|
||||||
|
import { isElectron } from "../../electron/electron";
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
@@ -68,26 +69,27 @@ export default function LoginScreen() {
|
|||||||
|
|
||||||
setCurrentPage("chat");
|
setCurrentPage("chat");
|
||||||
|
|
||||||
// Initialize push notifications
|
// Initialize notifications
|
||||||
try {
|
try {
|
||||||
if (pushNotificationManager.isSupported()) {
|
if (isSupported()) {
|
||||||
await pushNotificationManager.initialize();
|
const initialized = await initialize();
|
||||||
const permission = await pushNotificationManager.requestPermission();
|
if (initialized) {
|
||||||
|
await subscribe(data.token);
|
||||||
|
|
||||||
if (permission === "granted") {
|
// For Electron, start the notification receiver
|
||||||
const subscription = await pushNotificationManager.subscribe();
|
if (isElectron) {
|
||||||
if (subscription) {
|
await startElectronReceiver();
|
||||||
await pushNotificationManager.sendSubscriptionToServer(data.token);
|
}
|
||||||
console.log("Push notifications enabled");
|
|
||||||
|
console.log("Notifications enabled");
|
||||||
|
} else {
|
||||||
|
console.log("Notification permission denied");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Push notification permission denied");
|
console.log("Notifications not supported");
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("Not supported");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Push notification setup failed:", e);
|
console.error("Notification setup failed:", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data: ErrorResponse = await response.json();
|
const data: ErrorResponse = await response.json();
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import { API_BASE_URL } from "../core/config";
|
||||||
|
import { isElectron } from "../electron/electron";
|
||||||
|
import { websocket } from "../core/websocket";
|
||||||
|
import type { WebSocketMessage } from "../core/types";
|
||||||
|
|
||||||
|
export interface PushSubscriptionData {
|
||||||
|
endpoint: string;
|
||||||
|
keys: {
|
||||||
|
p256dh: string;
|
||||||
|
auth: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationPayload {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
icon?: string;
|
||||||
|
image?: string;
|
||||||
|
tag?: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
let isInitialized = false;
|
||||||
|
let registration: ServiceWorkerRegistration | null = null;
|
||||||
|
let subscription: PushSubscription | null = null;
|
||||||
|
let isElectronReceiverRunning = false;
|
||||||
|
let messageListener: ((event: MessageEvent) => void) | null = null;
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
|
const padding = "=".repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/-/g, "+")
|
||||||
|
.replace(/_/g, "/");
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribeToWebPush(): Promise<PushSubscription | null> {
|
||||||
|
if (!registration) {
|
||||||
|
throw new Error("Service Worker not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(
|
||||||
|
"BPFs0EYyE2XqAuY8vQ8B_ZggkJVhf9NmtKqSPtIKqy7lU0yGcM5qfpBz2ESRxNmC_CPbzoLbhKfF8fkKCFUwIjo"
|
||||||
|
).slice().buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Push subscription successful");
|
||||||
|
return subscription;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Push subscription failed:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendSubscriptionToServer(token: string): Promise<boolean> {
|
||||||
|
if (!subscription) {
|
||||||
|
throw new Error("No push subscription available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionData: PushSubscriptionData = {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: arrayBufferToBase64(subscription.getKey("p256dh")!),
|
||||||
|
auth: arrayBufferToBase64(subscription.getKey("auth")!)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/push/subscribe`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(subscriptionData)
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send subscription to server:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showMessageNotification(message: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
await showNotification({
|
||||||
|
title: `New message from ${message.username}`,
|
||||||
|
body: message.content.length > 100
|
||||||
|
? message.content.substring(0, 100) + "..."
|
||||||
|
: message.content,
|
||||||
|
icon: message.profile_picture || "/logo.png",
|
||||||
|
tag: `message_${message.id}`,
|
||||||
|
data: {
|
||||||
|
type: "public_message",
|
||||||
|
message_id: message.id,
|
||||||
|
sender_id: message.user_id,
|
||||||
|
sender_username: message.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to show message notification:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebSocketMessage(response: WebSocketMessage): Promise<void> {
|
||||||
|
// Handle notifications for new messages
|
||||||
|
if (response.type === "newMessage" && response.data) {
|
||||||
|
await showMessageNotification(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API functions
|
||||||
|
export async function initialize(): Promise<boolean> {
|
||||||
|
if (isInitialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isElectron) {
|
||||||
|
// For Electron, we just need to request permission
|
||||||
|
const permission = await window.electronInterface.notifications.requestPermission();
|
||||||
|
isInitialized = permission === "granted";
|
||||||
|
return isInitialized;
|
||||||
|
} else {
|
||||||
|
// For web browsers, initialize service worker and push manager
|
||||||
|
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
||||||
|
console.log("Push messaging is not supported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
registration = await navigator.serviceWorker.register("/assets/serviceWorker.js");
|
||||||
|
console.log("Service Worker registered successfully");
|
||||||
|
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
if (permission === "granted") {
|
||||||
|
await subscribeToWebPush();
|
||||||
|
isInitialized = true;
|
||||||
|
}
|
||||||
|
return isInitialized;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Service Worker registration failed:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize notification service:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subscribe(token: string): Promise<boolean> {
|
||||||
|
if (!isInitialized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isElectron) {
|
||||||
|
// In Electron, we don't need server-side subscription
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sendSubscriptionToServer(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showNotification(payload: NotificationPayload): Promise<boolean> {
|
||||||
|
if (isElectron) {
|
||||||
|
try {
|
||||||
|
return await window.electronInterface.notifications.show({
|
||||||
|
title: payload.title,
|
||||||
|
body: payload.body,
|
||||||
|
icon: payload.icon,
|
||||||
|
tag: payload.tag
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to show Electron notification:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For web browsers, notifications are handled by the service worker
|
||||||
|
// when push messages are received from the server
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unsubscribe(): Promise<boolean> {
|
||||||
|
if (isElectron) {
|
||||||
|
// In Electron, we don't need to unsubscribe from server
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await subscription.unsubscribe();
|
||||||
|
subscription = null;
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to unsubscribe:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSupported(): boolean {
|
||||||
|
if (isElectron) {
|
||||||
|
return true; // Electron always supports notifications
|
||||||
|
}
|
||||||
|
return "serviceWorker" in navigator && "PushManager" in window;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Electron-specific functions
|
||||||
|
export async function startElectronReceiver(): Promise<void> {
|
||||||
|
if (!isElectron || isElectronReceiverRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isElectronReceiverRunning = true;
|
||||||
|
|
||||||
|
// Add our own message listener to the existing WebSocket
|
||||||
|
messageListener = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const response: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
handleWebSocketMessage(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.addEventListener('message', messageListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopElectronReceiver(): void {
|
||||||
|
if (!isElectron) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isElectronReceiverRunning = false;
|
||||||
|
|
||||||
|
// Remove our message listener
|
||||||
|
if (messageListener) {
|
||||||
|
websocket.removeEventListener('message', messageListener);
|
||||||
|
messageListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { API_BASE_URL } from "../core/config";
|
|
||||||
|
|
||||||
export interface PushSubscriptionData {
|
|
||||||
endpoint: string;
|
|
||||||
keys: {
|
|
||||||
p256dh: string;
|
|
||||||
auth: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NotificationPayload {
|
|
||||||
title: string;
|
|
||||||
body: string;
|
|
||||||
icon?: string;
|
|
||||||
image?: string;
|
|
||||||
tag?: string;
|
|
||||||
data?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PushNotificationManager {
|
|
||||||
private registration: ServiceWorkerRegistration | null = null;
|
|
||||||
private subscription: PushSubscription | null = null;
|
|
||||||
|
|
||||||
async initialize(): Promise<boolean> {
|
|
||||||
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
|
||||||
console.log("Push messaging is not supported");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.registration = await navigator.serviceWorker.register("/assets/serviceWorker.js");
|
|
||||||
console.log("Service Worker registered successfully");
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Service Worker registration failed:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestPermission(): Promise<NotificationPermission> {
|
|
||||||
if (!this.registration) {
|
|
||||||
throw new Error("Service Worker not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = await Notification.requestPermission();
|
|
||||||
return permission;
|
|
||||||
}
|
|
||||||
|
|
||||||
async subscribe(): Promise<PushSubscription | null> {
|
|
||||||
if (!this.registration) {
|
|
||||||
throw new Error("Service Worker not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.subscription = await this.registration.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: this.urlBase64ToUint8Array(
|
|
||||||
"BPFs0EYyE2XqAuY8vQ8B_ZggkJVhf9NmtKqSPtIKqy7lU0yGcM5qfpBz2ESRxNmC_CPbzoLbhKfF8fkKCFUwIjo"
|
|
||||||
).slice().buffer
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Push subscription successful");
|
|
||||||
return this.subscription;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Push subscription failed:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendSubscriptionToServer(token: string): Promise<boolean> {
|
|
||||||
if (!this.subscription) {
|
|
||||||
throw new Error("No push subscription available");
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscriptionData: PushSubscriptionData = {
|
|
||||||
endpoint: this.subscription.endpoint,
|
|
||||||
keys: {
|
|
||||||
p256dh: this.arrayBufferToBase64(this.subscription.getKey("p256dh")!),
|
|
||||||
auth: this.arrayBufferToBase64(this.subscription.getKey("auth")!)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/push/subscribe`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(subscriptionData)
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.ok;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to send subscription to server:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async unsubscribe(): Promise<boolean> {
|
|
||||||
if (!this.subscription) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.subscription.unsubscribe();
|
|
||||||
this.subscription = null;
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to unsubscribe:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
||||||
const padding = "=".repeat((4 - base64String.length % 4) % 4);
|
|
||||||
const base64 = (base64String + padding)
|
|
||||||
.replace(/-/g, "+")
|
|
||||||
.replace(/_/g, "/");
|
|
||||||
|
|
||||||
const rawData = window.atob(base64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
||||||
const bytes = new Uint8Array(buffer);
|
|
||||||
let binary = "";
|
|
||||||
for (let i = 0; i < bytes.byteLength; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return window.btoa(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubscription(): PushSubscription | null {
|
|
||||||
return this.subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSupported(): boolean {
|
|
||||||
return "serviceWorker" in navigator && "PushManager" in window;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pushNotificationManager = new PushNotificationManager();
|
|
||||||
Reference in New Issue
Block a user