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:
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user