Some checks failed
CI / test (push) Failing after 21s
- 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
85 lines
2.2 KiB
TypeScript
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;
|
|
}
|
|
}
|