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:
2025-11-05 10:40:30 -03:00
parent 8ca737c62f
commit 1774b135b3
6 changed files with 853 additions and 18 deletions

View File

@@ -21,6 +21,10 @@
let messagesContainer: HTMLDivElement;
let shouldScrollToBottom = true;
let lastMessageCount = 0;
let lastMessageId = $state<string | null>(null);
let showNotificationPopup = $state(false);
let notificationMessage = $state<{ remetente: string; conteudo: string } | null>(null);
let notificationTimeout: ReturnType<typeof setTimeout> | null = null;
// Obter ID do usuário atual - usar $state para garantir reatividade
let usuarioAtualId = $state<string | null>(null);
@@ -36,12 +40,101 @@
}
});
// Função para tocar som de notificação
function tocarSomNotificacao() {
try {
// Usar AudioContext (requer interação do usuário para iniciar)
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
let audioContext: AudioContext | null = null;
try {
audioContext = new AudioContext();
} catch (e) {
// Se falhar, tentar resumir contexto existente
console.warn("Não foi possível criar AudioContext:", e);
return;
}
// Resumir contexto se estiver suspenso (necessário após interação do usuário)
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(() => {
// Ignorar erro se não conseguir resumir
});
} 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 (error) {
console.error("Erro ao tocar som de notificação:", error);
}
}
// Auto-scroll para a última mensagem quando novas mensagens chegam
// E detectar novas mensagens para tocar som e mostrar popup
$effect(() => {
if (mensagens?.data && messagesContainer) {
const currentCount = mensagens.data.length;
const isNewMessage = currentCount > lastMessageCount;
// Detectar nova mensagem de outro usuário
if (isNewMessage && mensagens.data.length > 0 && usuarioAtualId) {
const ultimaMensagem = mensagens.data[mensagens.data.length - 1];
const remetenteIdStr = ultimaMensagem.remetenteId
? String(ultimaMensagem.remetenteId).trim()
: (ultimaMensagem.remetente?._id ? String(ultimaMensagem.remetente._id).trim() : null);
// Se é uma nova mensagem de outro usuário (não minha)
if (remetenteIdStr && remetenteIdStr !== usuarioAtualId && ultimaMensagem._id !== lastMessageId) {
// Tocar som de notificação
tocarSomNotificacao();
// Mostrar popup de notificação
notificationMessage = {
remetente: ultimaMensagem.remetente?.nome || "Usuário",
conteudo: ultimaMensagem.conteudo.substring(0, 100) + (ultimaMensagem.conteudo.length > 100 ? "..." : "")
};
showNotificationPopup = true;
// Ocultar popup após 5 segundos
if (notificationTimeout) {
clearTimeout(notificationTimeout);
}
notificationTimeout = setTimeout(() => {
showNotificationPopup = false;
notificationMessage = null;
}, 5000);
lastMessageId = ultimaMensagem._id;
}
}
if (isNewMessage || shouldScrollToBottom) {
// Usar requestAnimationFrame para garantir que o DOM foi atualizado
requestAnimationFrame(() => {
@@ -536,3 +629,53 @@
{/if}
</div>
<!-- Popup de Notificação de Nova Mensagem -->
{#if showNotificationPopup && notificationMessage}
<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 animate-in slide-in-from-top-5 fade-in duration-300"
style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3);"
onclick={() => {
showNotificationPopup = false;
notificationMessage = null;
if (notificationTimeout) {
clearTimeout(notificationTimeout);
}
}}
>
<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 {notificationMessage.remetente}</p>
<p class="text-xs text-base-content/70 line-clamp-2">{notificationMessage.conteudo}</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();
showNotificationPopup = false;
notificationMessage = null;
if (notificationTimeout) {
clearTimeout(notificationTimeout);
}
}}
>
<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}