From 1774b135b3e2f90a3b282d2e9ba1b9f786144525 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Wed, 5 Nov 2025 10:40:30 -0300 Subject: [PATCH] feat: enhance chat functionality with global notifications and user management - Implemented global notifications for new messages, allowing users to receive alerts even when the chat is minimized or closed. - Added functionality for users to leave group conversations and meeting rooms, with appropriate notifications sent to remaining participants. - Introduced a modal for sending notifications within meeting rooms, enabling admins to communicate important messages to all participants. - Enhanced the chat components to support mention functionality, allowing users to tag participants in messages for better engagement. - Updated backend mutations to handle user exit from conversations and sending notifications, ensuring robust data handling and user experience. --- .../src/lib/components/chat/ChatWidget.svelte | 171 ++++++++++++ .../src/lib/components/chat/ChatWindow.svelte | 251 +++++++++++++++++- .../lib/components/chat/MessageInput.svelte | 130 ++++++++- .../lib/components/chat/MessageList.svelte | 143 ++++++++++ .../components/chat/SalaReuniaoManager.svelte | 6 +- packages/backend/convex/chat.ts | 170 ++++++++++++ 6 files changed, 853 insertions(+), 18 deletions(-) diff --git a/apps/web/src/lib/components/chat/ChatWidget.svelte b/apps/web/src/lib/components/chat/ChatWidget.svelte index f014d77..325db0c 100644 --- a/apps/web/src/lib/components/chat/ChatWidget.svelte +++ b/apps/web/src/lib/components/chat/ChatWidget.svelte @@ -7,6 +7,7 @@ minimizarChat, maximizarChat, abrirChat, + abrirConversa, } from "$lib/stores/chatStore"; import { useQuery } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; @@ -70,6 +71,9 @@ } let windowSize = $state(getSavedSize()); + let isMaximized = $state(false); + let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }); + let previousPosition = $state({ x: 0, y: 0 }); // Salvar tamanho no localStorage function saveSize() { @@ -152,6 +156,94 @@ activeConversation = $conversaAtiva; }); + // Detectar novas mensagens globalmente (mesmo quando chat está fechado/minimizado) + const todasConversas = useQuery(api.chat.listarConversas, {}); + let ultimoTimestampGlobal = $state(0); + let showGlobalNotificationPopup = $state(false); + let globalNotificationMessage = $state<{ remetente: string; conteudo: string; conversaId: string } | null>(null); + let globalNotificationTimeout: ReturnType | null = null; + + // Função para tocar som de notificação + function tocarSomNotificacaoGlobal() { + try { + const AudioContext = window.AudioContext || (window as any).webkitAudioContext; + const audioContext = new AudioContext(); + if (audioContext.state === 'suspended') { + audioContext.resume().then(() => { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillator.frequency.value = 800; + oscillator.type = 'sine'; + gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + }).catch(() => {}); + } else { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillator.frequency.value = 800; + oscillator.type = 'sine'; + gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + } + } catch (e) { + // Ignorar erro de áudio + } + } + + $effect(() => { + if (todasConversas?.data && authStore.usuario?._id) { + // Encontrar o maior timestamp de todas as conversas + let maiorTimestamp = 0; + let conversaComNovaMensagem: any = null; + + todasConversas.data.forEach((conv: any) => { + if (conv.ultimaMensagemTimestamp && conv.ultimaMensagemTimestamp > maiorTimestamp) { + maiorTimestamp = conv.ultimaMensagemTimestamp; + conversaComNovaMensagem = conv; + } + }); + + // Se há nova mensagem (timestamp maior) e não estamos vendo essa conversa, tocar som e mostrar popup + if (maiorTimestamp > ultimoTimestampGlobal && conversaComNovaMensagem) { + const conversaAtivaId = activeConversation ? String(activeConversation) : null; + const conversaIdStr = String(conversaComNovaMensagem._id); + + // Só mostrar notificação se não estamos vendo essa conversa + if (!isOpen || conversaAtivaId !== conversaIdStr) { + // Tocar som de notificação + tocarSomNotificacaoGlobal(); + + // Mostrar popup de notificação + globalNotificationMessage = { + remetente: conversaComNovaMensagem.outroUsuario?.nome || conversaComNovaMensagem.nome || "Usuário", + conteudo: conversaComNovaMensagem.ultimaMensagem || "", + conversaId: conversaComNovaMensagem._id + }; + showGlobalNotificationPopup = true; + + // Ocultar popup após 5 segundos + if (globalNotificationTimeout) { + clearTimeout(globalNotificationTimeout); + } + globalNotificationTimeout = setTimeout(() => { + showGlobalNotificationPopup = false; + globalNotificationMessage = null; + }, 5000); + } + + ultimoTimestampGlobal = maiorTimestamp; + } + } + }); + function handleToggle() { if (isOpen && !isMinimized) { minimizarChat(); @@ -169,6 +261,31 @@ } function handleMaximize() { + if (isMaximized) { + // Restaurar tamanho anterior + windowSize = previousSize; + position = previousPosition; + isMaximized = false; + saveSize(); + ajustarPosicao(); + } else { + // Salvar tamanho e posição atuais + previousSize = { ...windowSize }; + previousPosition = { ...position }; + + // Maximizar completamente: usar toda a largura e altura da tela + windowSize = { + width: window.innerWidth, + height: window.innerHeight + }; + position = { + x: 0, + y: 0 + }; + isMaximized = true; + saveSize(); + ajustarPosicao(); + } maximizarChat(); } @@ -546,6 +663,60 @@ {/if} + +{#if showGlobalNotificationPopup && globalNotificationMessage} +
{ + showGlobalNotificationPopup = false; + globalNotificationMessage = null; + if (globalNotificationTimeout) { + clearTimeout(globalNotificationTimeout); + } + // Abrir chat e conversa ao clicar + abrirChat(); + abrirConversa(globalNotificationMessage.conversaId as any); + }} + > +
+
+ + + +
+
+

Nova mensagem de {globalNotificationMessage.remetente}

+

{globalNotificationMessage.conteudo}

+

Clique para abrir

+
+ +
+
+{/if} +