mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement browser push notifications
This commit is contained in:
@@ -1,15 +1,56 @@
|
||||
import { useState } from "react";
|
||||
import { PRODUCT_NAME } from "../../../core/config";
|
||||
import { useState, useEffect } from "react";
|
||||
import { PRODUCT_NAME, API_BASE_URL } from "../../../core/config";
|
||||
import type { DialogProps } from "../../../core/types";
|
||||
import { MaterialDialog } from "../core/Dialog";
|
||||
import { pushNotificationManager } from "../../../utils/pushNotifications";
|
||||
import { useAppState } from "../../state";
|
||||
import type { Switch } from "mdui/components/switch";
|
||||
import { getAuthHeaders } from "../../../auth/api";
|
||||
|
||||
export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||
const [activePanel, setActivePanel] = useState("notifications-settings");
|
||||
const [pushNotificationsEnabled, setPushNotificationsEnabled] = useState(false);
|
||||
const [pushSupported, setPushSupported] = useState(false);
|
||||
const user = useAppState(state => state.user);
|
||||
|
||||
useEffect(() => {
|
||||
setPushSupported(pushNotificationManager.isSupported());
|
||||
setPushNotificationsEnabled(!!pushNotificationManager.getSubscription());
|
||||
}, []);
|
||||
|
||||
const handlePanelChange = (panelId: string) => {
|
||||
setActivePanel(panelId);
|
||||
};
|
||||
|
||||
const handlePushNotificationToggle = async (enabled: boolean) => {
|
||||
if (!user.authToken) return;
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
await pushNotificationManager.initialize();
|
||||
const permission = await pushNotificationManager.requestPermission();
|
||||
|
||||
if (permission === "granted") {
|
||||
const subscription = await pushNotificationManager.subscribe();
|
||||
if (subscription) {
|
||||
await pushNotificationManager.sendSubscriptionToServer(user.authToken);
|
||||
setPushNotificationsEnabled(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await pushNotificationManager.unsubscribe();
|
||||
// Call API to unsubscribe on server
|
||||
await fetch(`${API_BASE_URL}/push/unsubscribe`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(user.authToken)
|
||||
});
|
||||
setPushNotificationsEnabled(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle push notifications:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MaterialDialog close-on-overlay-click close-on-esc fullscreen open={isOpen} onOpenChange={onOpenChange} id="settings-dialog">
|
||||
<div className="fullscreen-wrapper">
|
||||
@@ -87,6 +128,14 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||
<div className="screen">
|
||||
<div id="notifications-settings" className={`settings-panel ${activePanel === "notifications-settings" ? "active" : ""}`}>
|
||||
<h3>Уведомления</h3>
|
||||
{pushSupported && (
|
||||
<mdui-switch
|
||||
checked={pushNotificationsEnabled}
|
||||
onInput={(e) => handlePushNotificationToggle((e.target as Switch).checked)}
|
||||
>
|
||||
Push уведомления
|
||||
</mdui-switch>
|
||||
)}
|
||||
<mdui-switch checked>Новые сообщения</mdui-switch>
|
||||
<mdui-switch checked>Звуковые уведомления</mdui-switch>
|
||||
<mdui-switch>Уведомления о статусе</mdui-switch>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useRef } from "react";
|
||||
import type { TextField } from "mdui/components/text-field";
|
||||
import { useAppState } from "../state";
|
||||
import { MaterialTextField } from "../components/core/TextField";
|
||||
import { pushNotificationManager } from "../../utils/pushNotifications";
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||
@@ -65,6 +66,26 @@ export default function LoginScreen() {
|
||||
console.error("Key setup failed:", e);
|
||||
}
|
||||
|
||||
// Initialize push notifications
|
||||
try {
|
||||
if (pushNotificationManager.isSupported()) {
|
||||
await pushNotificationManager.initialize();
|
||||
const permission = await pushNotificationManager.requestPermission();
|
||||
|
||||
if (permission === "granted") {
|
||||
const subscription = await pushNotificationManager.subscribe();
|
||||
if (subscription) {
|
||||
await pushNotificationManager.sendSubscriptionToServer(data.token);
|
||||
console.log("Push notifications enabled");
|
||||
}
|
||||
} else {
|
||||
console.log("Push notification permission denied");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Push notification setup failed:", e);
|
||||
}
|
||||
|
||||
setCurrentPage("chat");
|
||||
// initializeProfile(); // Initialize profile after login
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'mdui/components/text-field';
|
||||
import 'mdui/components/button-icon';
|
||||
import 'mdui/components/top-app-bar';
|
||||
import 'mdui/components/top-app-bar-title';
|
||||
import 'mdui/components/switch';
|
||||
|
||||
import { setColorScheme } from 'mdui/functions/setColorScheme.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
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("/sw.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