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.
This commit is contained in:
@@ -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<number>(0);
|
||||
let showGlobalNotificationPopup = $state(false);
|
||||
let globalNotificationMessage = $state<{ remetente: string; conteudo: string; conversaId: string } | null>(null);
|
||||
let globalNotificationTimeout: ReturnType<typeof setTimeout> | 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 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Popup Global de Notificação de Nova Mensagem (quando chat está fechado/minimizado) -->
|
||||
{#if showGlobalNotificationPopup && globalNotificationMessage}
|
||||
<div
|
||||
class="fixed top-4 right-4 z-[1000] bg-base-100 rounded-lg shadow-2xl border border-primary/20 p-4 max-w-sm cursor-pointer"
|
||||
style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
|
||||
onclick={() => {
|
||||
showGlobalNotificationPopup = false;
|
||||
globalNotificationMessage = null;
|
||||
if (globalNotificationTimeout) {
|
||||
clearTimeout(globalNotificationTimeout);
|
||||
}
|
||||
// Abrir chat e conversa ao clicar
|
||||
abrirChat();
|
||||
abrirConversa(globalNotificationMessage.conversaId as any);
|
||||
}}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 text-primary"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-semibold text-base-content text-sm mb-1">Nova mensagem de {globalNotificationMessage.remetente}</p>
|
||||
<p class="text-xs text-base-content/70 line-clamp-2">{globalNotificationMessage.conteudo}</p>
|
||||
<p class="text-xs text-primary mt-1">Clique para abrir</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-shrink-0 w-6 h-6 rounded-full hover:bg-base-200 flex items-center justify-center transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
showGlobalNotificationPopup = false;
|
||||
globalNotificationMessage = null;
|
||||
if (globalNotificationTimeout) {
|
||||
clearTimeout(globalNotificationTimeout);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Animação do badge com bounce suave */
|
||||
@keyframes badge-bounce {
|
||||
|
||||
Reference in New Issue
Block a user