Files
Messenger/web/src/utils/webNotifications.ts
benya 4fe89ce89a
Some checks failed
CI / test (push) Failing after 21s
feat(web): service-worker notifications and composer/scroll UX fixes
- register notifications service worker and handle click-to-open chat/message

- route realtime notifications through service worker with fallback

- support ?chat=&message= deep-link navigation in chats page

- enforce 1s minimum voice message length

- lift scroll-to-bottom button to avoid overlap with composer action
2026-03-08 11:33:58 +03:00

85 lines
2.2 KiB
TypeScript

import type { Message } from "../chat/types";
export async function registerNotificationServiceWorker(): Promise<void> {
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
return;
}
try {
await navigator.serviceWorker.register("/notifications-sw.js");
} catch {
return;
}
}
export async function showNotificationViaServiceWorker(params: {
chatId: number;
messageId: number;
title: string;
body: string;
image?: string;
}): Promise<boolean> {
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
return false;
}
try {
const registration = await navigator.serviceWorker.getRegistration();
if (!registration) {
return false;
}
const url = `/?chat=${params.chatId}&message=${params.messageId}`;
await registration.showNotification(params.title, {
body: params.body,
icon: params.image,
tag: `chat-${params.chatId}`,
data: {
chatId: params.chatId,
messageId: params.messageId,
url,
},
});
return true;
} catch {
return false;
}
}
export function messagePreviewByType(type: Message["type"]): string {
if (type === "image") return "Photo";
if (type === "video") return "Video";
if (type === "audio") return "Audio";
if (type === "voice") return "Voice message";
if (type === "file") return "File";
if (type === "circle_video") return "Video message";
return "New message";
}
export function buildNotificationPreview(
message: Message,
withPreview: boolean
): { body: string; image?: string } {
if (!withPreview) {
return { body: "New message" };
}
if (message.type !== "text") {
if (message.type === "image") {
const imageUrl = typeof message.text === "string" && isLikelyUrl(message.text) ? message.text : undefined;
return { body: "🖼 Photo", image: imageUrl };
}
return { body: messagePreviewByType(message.type) };
}
const text = message.text?.trim();
if (!text) {
return { body: "New message" };
}
return { body: text };
}
function isLikelyUrl(value: string): boolean {
try {
const parsed = new URL(value);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}