refactor: enhance chat components with type safety and response functionality

- Updated type definitions in ChatWindow and MessageList components for better type safety.
- Improved MessageInput to handle message responses, including a preview feature for replying to messages.
- Enhanced the chat message handling logic to support message references and improve user interaction.
- Refactored notification utility functions to support push notifications and rate limiting for email sending.
- Updated backend schema to accommodate new features related to message responses and notifications.
This commit is contained in:
2025-11-04 20:36:01 -03:00
parent 15374276d5
commit 12db52a8a7
23 changed files with 3195 additions and 503 deletions

View File

@@ -1,66 +1,207 @@
/**
* Solicita permissão para notificações desktop
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!("Notification" in window)) {
console.warn("Este navegador não suporta notificações desktop");
return "denied";
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission !== "denied") {
return await Notification.requestPermission();
}
return Notification.permission;
}
/**
* Mostra uma notificação desktop
*/
export function showNotification(title: string, options?: NotificationOptions): Notification | null {
if (!("Notification" in window)) {
return null;
}
if (Notification.permission !== "granted") {
return null;
}
try {
return new Notification(title, {
icon: "/favicon.png",
badge: "/favicon.png",
...options,
});
} catch (error) {
console.error("Erro ao exibir notificação:", error);
return null;
}
}
/**
* Toca o som de notificação
*/
export function playNotificationSound() {
try {
const audio = new Audio("/sounds/notification.mp3");
audio.volume = 0.5;
audio.play().catch((err) => {
console.warn("Não foi possível reproduzir o som de notificação:", err);
});
} catch (error) {
console.error("Erro ao tocar som de notificação:", error);
}
}
/**
* Verifica se o usuário está na aba ativa
*/
export function isTabActive(): boolean {
return !document.hidden;
}
/**
* Solicita permissão para notificações desktop
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!("Notification" in window)) {
console.warn("Este navegador não suporta notificações desktop");
return "denied";
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission !== "denied") {
return await Notification.requestPermission();
}
return Notification.permission;
}
/**
* Mostra uma notificação desktop
*/
export function showNotification(title: string, options?: NotificationOptions): Notification | null {
if (!("Notification" in window)) {
return null;
}
if (Notification.permission !== "granted") {
return null;
}
try {
return new Notification(title, {
icon: "/favicon.png",
badge: "/favicon.png",
...options,
});
} catch (error) {
console.error("Erro ao exibir notificação:", error);
return null;
}
}
/**
* Toca o som de notificação
*/
export function playNotificationSound() {
try {
const audio = new Audio("/sounds/notification.mp3");
audio.volume = 0.5;
audio.play().catch((err) => {
console.warn("Não foi possível reproduzir o som de notificação:", err);
});
} catch (error) {
console.error("Erro ao tocar som de notificação:", error);
}
}
/**
* Verifica se o usuário está na aba ativa
*/
export function isTabActive(): boolean {
return !document.hidden;
}
/**
* Registrar service worker para push notifications
*/
export async function registrarServiceWorker(): Promise<ServiceWorkerRegistration | null> {
if (!("serviceWorker" in navigator)) {
console.warn("Service Workers não são suportados neste navegador");
return null;
}
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
console.log("Service Worker registrado:", registration);
return registration;
} catch (error) {
console.error("Erro ao registrar Service Worker:", error);
return null;
}
}
/**
* Solicitar subscription de push notification
*/
export async function solicitarPushSubscription(): Promise<PushSubscription | null> {
// Registrar service worker primeiro
const registration = await registrarServiceWorker();
if (!registration) {
return null;
}
// Verificar se push está disponível
if (!("PushManager" in window)) {
console.warn("Push notifications não são suportadas neste navegador");
return null;
}
// Solicitar permissão
const permission = await requestNotificationPermission();
if (permission !== "granted") {
console.warn("Permissão para notificações negada");
return null;
}
try {
// Obter subscription existente ou criar nova
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
// VAPID public key deve vir do backend ou config
// Por enquanto, usando uma chave pública de exemplo
// Em produção, isso deve vir de uma variável de ambiente ou API
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY || "";
if (!vapidPublicKey) {
console.warn("VAPID public key não configurada");
return null;
}
// Converter chave para formato Uint8Array
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
});
}
return subscription;
} catch (error) {
console.error("Erro ao obter subscription:", error);
return null;
}
}
/**
* Converter chave VAPID de base64 URL-safe para Uint8Array
*/
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;
}
/**
* Converter PushSubscription para formato serializável
*/
export function subscriptionToJSON(subscription: PushSubscription): {
endpoint: string;
keys: { p256dh: string; auth: string };
} {
const key = subscription.getKey("p256dh");
const auth = subscription.getKey("auth");
if (!key || !auth) {
throw new Error("Chaves de subscription não encontradas");
}
return {
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(key),
auth: arrayBufferToBase64(auth),
},
};
}
/**
* Converter ArrayBuffer para base64
*/
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);
}
/**
* Remover subscription de push notification
*/
export async function removerPushSubscription(): Promise<boolean> {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
return true;
}
return false;
}