From 9a5f2b294d658f49fd5419cf954f2d8614ac5829 Mon Sep 17 00:00:00 2001 From: killer-cf Date: Sat, 8 Nov 2025 10:52:33 -0300 Subject: [PATCH] refactor: integrate current user data across components - Replaced instances of `authStore` with `currentUser` to streamline user authentication handling. - Updated permission checks and user-related data retrieval to utilize the new `useQuery` for better performance and clarity. - Cleaned up component structures and improved formatting for consistency and readability. - Enhanced error handling and user feedback mechanisms in various components to improve user experience. --- .../web/src/lib/components/ActionGuard.svelte | 16 +- apps/web/src/lib/components/FileUpload.svelte | 95 +-- .../src/lib/components/MenuProtection.svelte | 59 +- .../lib/components/ModelosDeclaracoes.svelte | 122 ++-- .../src/lib/components/ProtectedRoute.svelte | 24 +- .../components/PushNotificationManager.svelte | 65 +- apps/web/src/lib/components/Sidebar.svelte | 8 +- .../src/lib/components/chat/ChatList.svelte | 6 +- .../src/lib/components/chat/ChatWidget.svelte | 583 +++++++++++------- .../src/lib/components/chat/ChatWindow.svelte | 204 ++++-- .../lib/components/chat/MessageInput.svelte | 206 +++++-- .../lib/components/chat/MessageList.svelte | 461 ++++++++------ .../chat/NewConversationModal.svelte | 149 +++-- .../components/chat/NotificationBell.svelte | 31 +- .../components/chat/PresenceManager.svelte | 1 - .../components/chat/SalaReuniaoManager.svelte | 138 +++-- .../chat/ScheduleMessageModal.svelte | 73 ++- .../(dashboard)/alterar-senha/+page.svelte | 215 +++++-- .../gestao-ausencias/+page.svelte | 6 +- .../routes/(dashboard)/perfil/+page.svelte | 103 ++-- .../recursos-humanos/ausencias/+page.svelte | 42 +- .../funcionarios/[funcionarioId]/+page.svelte | 2 +- .../gestao-ausencias/+page.svelte | 35 +- .../ti/configuracoes-email/+page.svelte | 512 ++++++++------- .../(dashboard)/ti/notificacoes/+page.svelte | 40 +- .../ti/painel-permissoes/+page.svelte | 1 - .../routes/(dashboard)/ti/times/+page.svelte | 1 - .../(dashboard)/ti/usuarios/+page.svelte | 349 ++++++++--- 28 files changed, 2312 insertions(+), 1235 deletions(-) diff --git a/apps/web/src/lib/components/ActionGuard.svelte b/apps/web/src/lib/components/ActionGuard.svelte index d88ca22..a616643 100644 --- a/apps/web/src/lib/components/ActionGuard.svelte +++ b/apps/web/src/lib/components/ActionGuard.svelte @@ -2,9 +2,8 @@ import { useQuery } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; - import { authStore } from "$lib/stores/auth.svelte"; import { loginModalStore } from "$lib/stores/loginModal.svelte"; - import { AlertTriangle } from "lucide-svelte"; + import { TriangleAlert } from "lucide-svelte"; interface Props { recurso: string; @@ -17,18 +16,21 @@ let verificando = $state(true); let permitido = $state(false); + // Usuário atual + const currentUser = useQuery(api.auth.getCurrentUser, {}); + const permissaoQuery = $derived( - authStore.usuario + currentUser?.data ? useQuery(api.permissoesAcoes.verificarAcao, { - usuarioId: authStore.usuario._id as Id<"usuarios">, + usuarioId: currentUser.data._id as Id<"usuarios">, recurso, acao, }) - : null + : null, ); $effect(() => { - if (!authStore.autenticado) { + if (!currentUser?.data) { verificando = false; permitido = false; const currentPath = window.location.pathname; @@ -60,7 +62,7 @@
- +

Acesso Negado

diff --git a/apps/web/src/lib/components/FileUpload.svelte b/apps/web/src/lib/components/FileUpload.svelte index 2f3227c..442fdbc 100644 --- a/apps/web/src/lib/components/FileUpload.svelte +++ b/apps/web/src/lib/components/FileUpload.svelte @@ -1,7 +1,15 @@ - diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index b394bd1..f184db7 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -3,7 +3,6 @@ import { goto } from "$app/navigation"; import logo from "$lib/assets/logo_governo_PE.png"; import type { Snippet } from "svelte"; - import { authStore } from "$lib/stores/auth.svelte"; import { loginModalStore } from "$lib/stores/loginModal.svelte"; import { useQuery } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; @@ -254,7 +253,7 @@ {#if avatarUrlDoUsuario()} {authStore.usuario?.nome {:else} @@ -277,8 +276,7 @@ class="dropdown-content z-1 menu p-2 shadow-xl bg-base-100 rounded-box w-52 mt-4 border border-primary/20" >

  • Meu Perfil
  • Alterar Senha
  • @@ -629,7 +627,7 @@ {/if} -{#if authStore.autenticado} +{#if currentUser.data} {/if} diff --git a/apps/web/src/lib/components/chat/ChatList.svelte b/apps/web/src/lib/components/chat/ChatList.svelte index 2ee80b8..4468fe3 100644 --- a/apps/web/src/lib/components/chat/ChatList.svelte +++ b/apps/web/src/lib/components/chat/ChatList.svelte @@ -291,7 +291,7 @@ >
    -
    +
    { - const usuario = authStore.usuario; + const usuario = currentUser?.data; if (!usuario) return null; - + // Prioridade: fotoPerfilUrl > avatar > fallback com nome - if (usuario.fotoPerfilUrl) { - return usuario.fotoPerfilUrl; + if (usuario.fotoPerfil) { + return usuario.fotoPerfil; } if (usuario.avatar) { return getAvatarUrl(usuario.avatar); @@ -62,14 +63,21 @@ // Carregar tamanho salvo do localStorage ou usar padrão function getSavedSize() { - if (typeof window === 'undefined') return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; - const saved = localStorage.getItem('chat-window-size'); + if (typeof window === "undefined") + return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; + const saved = localStorage.getItem("chat-window-size"); if (saved) { try { const parsed = JSON.parse(saved); return { - width: Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH)), - height: Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT)) + width: Math.max( + MIN_WIDTH, + Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH), + ), + height: Math.max( + MIN_HEIGHT, + Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT), + ), }; } catch { return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; @@ -85,47 +93,47 @@ // Dimensões da janela (reativo) let windowDimensions = $state({ width: 0, height: 0 }); - + // Atualizar dimensões da janela function updateWindowDimensions() { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { windowDimensions = { width: window.innerWidth, - height: window.innerHeight + height: window.innerHeight, }; } } // Inicializar e atualizar dimensões da janela $effect(() => { - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + updateWindowDimensions(); - + // Inicializar posição apenas uma vez quando as dimensões estiverem disponíveis if (position === null) { - const saved = localStorage.getItem('chat-widget-position'); + const saved = localStorage.getItem("chat-widget-position"); if (saved) { try { const parsed = JSON.parse(saved); position = parsed; } catch { // Se falhar ao parsear, usar posição padrão no canto inferior direito - position = { + position = { x: window.innerWidth - 72 - 24, - y: window.innerHeight - 72 - 24 + y: window.innerHeight - 72 - 24, }; } } else { // Posição padrão: canto inferior direito - position = { + position = { x: window.innerWidth - 72 - 24, - y: window.innerHeight - 72 - 24 + y: window.innerHeight - 72 - 24, }; } savePosition(); // Salvar posição inicial } - + const handleResize = () => { updateWindowDimensions(); // Ajustar posição quando a janela redimensionar @@ -133,25 +141,25 @@ ajustarPosicao(); } }; - - window.addEventListener('resize', handleResize); - + + window.addEventListener("resize", handleResize); + return () => { - window.removeEventListener('resize', handleResize); + window.removeEventListener("resize", handleResize); }; }); - + // Salvar posição no localStorage function savePosition() { - if (typeof window !== 'undefined' && position) { - localStorage.setItem('chat-widget-position', JSON.stringify(position)); + if (typeof window !== "undefined" && position) { + localStorage.setItem("chat-widget-position", JSON.stringify(position)); } } // Salvar tamanho no localStorage function saveSize() { - if (typeof window !== 'undefined') { - localStorage.setItem('chat-window-size', JSON.stringify(windowSize)); + if (typeof window !== "undefined") { + localStorage.setItem("chat-window-size", JSON.stringify(windowSize)); } } @@ -170,9 +178,9 @@ x: e.clientX, y: e.clientY, width: windowSize.width, - height: windowSize.height + height: windowSize.height, }; - document.body.classList.add('resizing'); + document.body.classList.add("resizing"); } function handleResizeMove(e: MouseEvent) { @@ -187,20 +195,32 @@ let newY = position.y; // Redimensionar baseado na direção - if (resizeDirection.includes('e')) { - newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX)); + if (resizeDirection.includes("e")) { + newWidth = Math.max( + MIN_WIDTH, + Math.min(MAX_WIDTH, resizeStart.width + deltaX), + ); } - if (resizeDirection.includes('w')) { - const calculatedWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX)); + if (resizeDirection.includes("w")) { + const calculatedWidth = Math.max( + MIN_WIDTH, + Math.min(MAX_WIDTH, resizeStart.width - deltaX), + ); const widthDelta = resizeStart.width - calculatedWidth; newWidth = calculatedWidth; newX = position.x + widthDelta; } - if (resizeDirection.includes('s')) { - newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY)); + if (resizeDirection.includes("s")) { + newHeight = Math.max( + MIN_HEIGHT, + Math.min(MAX_HEIGHT, resizeStart.height + deltaY), + ); } - if (resizeDirection.includes('n')) { - const calculatedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY)); + if (resizeDirection.includes("n")) { + const calculatedHeight = Math.max( + MIN_HEIGHT, + Math.min(MAX_HEIGHT, resizeStart.height - deltaY), + ); const heightDelta = resizeStart.height - calculatedHeight; newHeight = calculatedHeight; newY = position.y + heightDelta; @@ -214,7 +234,7 @@ if (isResizing) { isResizing = false; resizeDirection = null; - document.body.classList.remove('resizing'); + document.body.classList.remove("resizing"); saveSize(); ajustarPosicao(); } @@ -231,13 +251,15 @@ $effect(() => { activeConversation = $conversaAtiva; - + // Quando uma conversa é aberta, marcar suas mensagens como visualizadas // para evitar notificações repetidas quando a conversa já está aberta - if (activeConversation && todasConversas?.data && authStore.usuario?._id) { + if (activeConversation && todasConversas?.data && currentUser?.data?._id) { const conversas = todasConversas.data as ConversaComTimestamp[]; - const conversaAberta = conversas.find((c) => String(c._id) === String(activeConversation)); - + const conversaAberta = conversas.find( + (c) => String(c._id) === String(activeConversation), + ); + if (conversaAberta && conversaAberta.ultimaMensagemTimestamp) { const mensagemId = `${conversaAberta._id}-${conversaAberta.ultimaMensagemTimestamp}`; if (!mensagensNotificadasGlobal.has(mensagemId)) { @@ -253,17 +275,21 @@ $effect(() => { if (isOpen && !isMinimized && position && wasPreviouslyClosed) { // Quando a janela é aberta, recalcular posição para garantir que fique visível - const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0); - const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0); + const winHeight = + windowDimensions.height || + (typeof window !== "undefined" ? window.innerHeight : 0); + const winWidth = + windowDimensions.width || + (typeof window !== "undefined" ? window.innerWidth : 0); const widgetHeight = windowSize.height; const widgetWidth = windowSize.width; - + // Calcular limites válidos para a janela grande const minY = -(widgetHeight - 100); const maxY = Math.max(0, winHeight - 100); const minX = -(widgetWidth - 100); const maxX = Math.max(0, winWidth - 100); - + // Recalcular posição Y: tentar manter próximo ao canto inferior direito mas ajustar se necessário let newY = position.y; // Se a posição Y estava calculada para um botão pequeno (72px), ajustar para janela grande @@ -272,10 +298,10 @@ // Se estava muito baixo (valor grande), ajustar para uma posição válida newY = Math.max(minY, Math.min(maxY, winHeight - widgetHeight - 24)); } - + // Garantir que X também está dentro dos limites let newX = Math.max(minX, Math.min(maxX, position.x)); - + // Aplicar novos valores apenas se necessário if (newX !== position.x || newY !== position.y) { position = { x: newX, y: newY }; @@ -283,7 +309,7 @@ // Forçar ajuste imediatamente ajustarPosicao(); } - + wasPreviouslyClosed = false; } else if (!isOpen || isMinimized) { wasPreviouslyClosed = true; @@ -304,15 +330,19 @@ const todasConversas = useQuery(api.chat.listarConversas, {}); let mensagensNotificadasGlobal = $state>(new Set()); let showGlobalNotificationPopup = $state(false); - let globalNotificationMessage = $state<{ remetente: string; conteudo: string; conversaId: string } | null>(null); + let globalNotificationMessage = $state<{ + remetente: string; + conteudo: string; + conversaId: string; + } | null>(null); let globalNotificationTimeout: ReturnType | null = null; - + // Carregar mensagens já notificadas do localStorage ao montar let mensagensCarregadasGlobal = $state(false); - + $effect(() => { - if (typeof window !== 'undefined' && !mensagensCarregadasGlobal) { - const saved = localStorage.getItem('chat-mensagens-notificadas-global'); + if (typeof window !== "undefined" && !mensagensCarregadasGlobal) { + const saved = localStorage.getItem("chat-mensagens-notificadas-global"); if (saved) { try { const ids = JSON.parse(saved) as string[]; @@ -322,7 +352,7 @@ } } mensagensCarregadasGlobal = true; - + // Marcar todas as mensagens atuais como já visualizadas (não tocar beep ao abrir) if (todasConversas?.data) { const conversas = todasConversas.data as ConversaComTimestamp[]; @@ -339,43 +369,58 @@ // Salvar mensagens notificadas no localStorage function salvarMensagensNotificadasGlobal() { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { const ids = Array.from(mensagensNotificadasGlobal); // Limitar a 1000 IDs para não encher o localStorage const idsLimitados = ids.slice(-1000); - localStorage.setItem('chat-mensagens-notificadas-global', JSON.stringify(idsLimitados)); + localStorage.setItem( + "chat-mensagens-notificadas-global", + JSON.stringify(idsLimitados), + ); } } - + // Função para tocar som de notificação function tocarSomNotificacaoGlobal() { try { - const AudioContextClass = window.AudioContext || (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; + const AudioContextClass = + window.AudioContext || + (window as { webkitAudioContext?: typeof AudioContext }) + .webkitAudioContext; if (!AudioContextClass) return; - + const audioContext = new AudioContextClass(); - 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(() => {}); + 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'; + oscillator.type = "sine"; gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + gainNode.gain.exponentialRampToValueAtTime( + 0.01, + audioContext.currentTime + 0.3, + ); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } @@ -383,20 +428,20 @@ // Ignorar erro de áudio } } - + $effect(() => { - if (todasConversas?.data && authStore.usuario?._id) { + if (todasConversas?.data && currentUser?.data?._id) { const conversas = todasConversas.data as ConversaComTimestamp[]; - + // Encontrar conversas com novas mensagens // Obter ID do usuário logado de forma robusta // Prioridade: usar query do Convex (mais confiável) > authStore - const usuarioLogado = authStore.usuario; + const usuarioLogado = currentUser?.data; const perfilConvex = meuPerfilQuery?.data; - + // Usar ID do Convex se disponível, caso contrário usar authStore let meuId: string | null = null; - + if (perfilConvex && perfilConvex._id) { // Usar ID retornado pela query do Convex (mais confiável) meuId = String(perfilConvex._id).trim(); @@ -404,49 +449,55 @@ // Fallback para authStore meuId = String(usuarioLogado._id).trim(); } - + if (!meuId) { - console.warn("⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:", { - authStore: !!usuarioLogado, - authStoreId: usuarioLogado?._id, - convexPerfil: !!perfilConvex, - convexId: perfilConvex?._id - }); + console.warn( + "⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:", + { + currentUser: !!usuarioLogado, + currentUserId: usuarioLogado?._id, + convexPerfil: !!perfilConvex, + convexId: perfilConvex?._id, + }, + ); return; } - + // Log para debug (apenas em desenvolvimento) if (import.meta.env.DEV) { console.log("🔍 [ChatWidget] Usuário logado identificado:", { id: meuId, - fonte: perfilConvex ? "Convex Query" : "AuthStore", + fonte: perfilConvex ? "Convex Query" : "CurrentUser", nome: usuarioLogado?.nome || perfilConvex?.nome, - email: usuarioLogado?.email + email: usuarioLogado?.email, }); } - + conversas.forEach((conv) => { if (!conv.ultimaMensagemTimestamp) return; - + // Verificar se a última mensagem foi enviada pelo usuário atual // Comparação mais robusta: normalizar ambos os IDs para string e comparar - const remetenteIdStr = conv.ultimaMensagemRemetenteId - ? String(conv.ultimaMensagemRemetenteId).trim() + const remetenteIdStr = conv.ultimaMensagemRemetenteId + ? String(conv.ultimaMensagemRemetenteId).trim() : null; - + // Log para debug da comparação (apenas em desenvolvimento) if (import.meta.env.DEV && remetenteIdStr) { const ehMinhaMensagem = remetenteIdStr === meuId; if (ehMinhaMensagem) { - console.log("✅ [ChatWidget] Mensagem identificada como própria (ignorada):", { - conversaId: conv._id, - meuId, - remetenteId: remetenteIdStr, - mensagem: conv.ultimaMensagem?.substring(0, 50) - }); + console.log( + "✅ [ChatWidget] Mensagem identificada como própria (ignorada):", + { + conversaId: conv._id, + meuId, + remetenteId: remetenteIdStr, + mensagem: conv.ultimaMensagem?.substring(0, 50), + }, + ); } } - + // Se a mensagem foi enviada pelo próprio usuário, ignorar completamente if (remetenteIdStr && remetenteIdStr === meuId) { // Mensagem enviada pelo próprio usuário - não tocar beep nem mostrar notificação @@ -458,17 +509,19 @@ } return; } - + // Criar ID único para esta mensagem: conversaId-timestamp const mensagemId = `${conv._id}-${conv.ultimaMensagemTimestamp}`; - + // Verificar se já foi notificada if (mensagensNotificadasGlobal.has(mensagemId)) return; - - const conversaAtivaId = activeConversation ? String(activeConversation).trim() : null; + + const conversaAtivaId = activeConversation + ? String(activeConversation).trim() + : null; const conversaIdStr = String(conv._id).trim(); const estaConversaEstaAberta = conversaAtivaId === conversaIdStr; - + // Só mostrar notificação se: // 1. O chat não está aberto OU // 2. O chat está aberto mas não estamos vendo essa conversa específica @@ -476,18 +529,18 @@ // Marcar como notificada ANTES de mostrar notificação (evita duplicação) mensagensNotificadasGlobal.add(mensagemId); salvarMensagensNotificadasGlobal(); - + // Tocar som de notificação (apenas uma vez) tocarSomNotificacaoGlobal(); - + // Mostrar popup de notificação globalNotificationMessage = { remetente: conv.outroUsuario?.nome || conv.nome || "Usuário", conteudo: conv.ultimaMensagem || "", - conversaId: conv._id + conversaId: conv._id, }; showGlobalNotificationPopup = true; - + // Ocultar popup após 5 segundos if (globalNotificationTimeout) { clearTimeout(globalNotificationTimeout); @@ -524,7 +577,7 @@ function handleMaximize() { if (!position) return; - + if (isMaximized) { // Restaurar tamanho anterior windowSize = previousSize; @@ -538,18 +591,22 @@ // Salvar tamanho e posição atuais previousSize = { ...windowSize }; previousPosition = { ...position }; - + // Maximizar completamente: usar toda a largura e altura da tela - const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : DEFAULT_WIDTH); - const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : DEFAULT_HEIGHT); - + const winWidth = + windowDimensions.width || + (typeof window !== "undefined" ? window.innerWidth : DEFAULT_WIDTH); + const winHeight = + windowDimensions.height || + (typeof window !== "undefined" ? window.innerHeight : DEFAULT_HEIGHT); + windowSize = { width: winWidth, - height: winHeight + height: winHeight, }; position = { x: 0, - y: 0 + y: 0, }; isMaximized = true; saveSize(); @@ -564,15 +621,15 @@ hasMoved = false; shouldPreventClick = false; isDragging = true; - + // Calcular offset do clique dentro do elemento (considerando a posição atual) // Isso garante que o arrasto comece exatamente onde o usuário clicou dragStart = { x: e.clientX - position.x, y: e.clientY - position.y, }; - - document.body.classList.add('dragging'); + + document.body.classList.add("dragging"); e.preventDefault(); } @@ -583,14 +640,14 @@ hasMoved = false; shouldPreventClick = false; isDragging = true; - + // Calcular offset do clique exatamente onde o mouse está dragStart = { x: e.clientX - position.x, y: e.clientY - position.y, }; - - document.body.classList.add('dragging'); + + document.body.classList.add("dragging"); // Não prevenir default para permitir clique funcionar se não houver movimento } @@ -599,17 +656,17 @@ handleResizeMove(e); return; } - + if (!isDragging || !position) return; - + // Calcular nova posição baseada no offset do clique const newX = e.clientX - dragStart.x; const newY = e.clientY - dragStart.y; - + // Verificar se houve movimento significativo desde o último frame const deltaX = Math.abs(newX - position.x); const deltaY = Math.abs(newY - position.y); - + // Se houve qualquer movimento (mesmo pequeno), marcar como movido if (deltaX > 0 || deltaY > 0) { // Marcar como movido se passar do threshold @@ -617,21 +674,25 @@ hasMoved = true; shouldPreventClick = true; // Prevenir clique se houve movimento } - + // Dimensões do widget const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72; const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72; - + // Usar dimensões reativas da janela - const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0); - const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0); - + const winWidth = + windowDimensions.width || + (typeof window !== "undefined" ? window.innerWidth : 0); + const winHeight = + windowDimensions.height || + (typeof window !== "undefined" ? window.innerHeight : 0); + // Limites da tela com margem de segurança const minX = -(widgetWidth - 100); // Permitir até 100px visíveis const maxX = Math.max(0, winWidth - 100); // Manter 100px dentro da tela const minY = -(widgetHeight - 100); const maxY = Math.max(0, winHeight - 100); - + // Atualizar posição imediatamente - garantir suavidade position = { x: Math.max(minX, Math.min(newX, maxX)), @@ -643,77 +704,81 @@ function handleMouseUp(e?: MouseEvent) { const hadMoved = hasMoved; const shouldPrevent = shouldPreventClick; - + if (isDragging) { isDragging = false; - + // Se estava arrastando e houve movimento, prevenir clique if (hadMoved && e) { e.preventDefault(); e.stopPropagation(); } - + // Garantir que está dentro dos limites ao soltar ajustarPosicao(); - + // Salvar posição após arrastar savePosition(); - + // Aguardar um pouco antes de resetar as flags para garantir que o onclick não seja executado setTimeout(() => { hasMoved = false; shouldPreventClick = false; }, 100); - - document.body.classList.remove('dragging'); + + document.body.classList.remove("dragging"); } handleResizeEnd(); - + return !hadMoved; // Retorna true se não houve movimento (permite clique) } function ajustarPosicao() { if (!position) return; - + isAnimating = true; - + // Dimensões do widget const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72; const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72; - + // Usar dimensões reativas da janela - const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0); - const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0); - + const winWidth = + windowDimensions.width || + (typeof window !== "undefined" ? window.innerWidth : 0); + const winHeight = + windowDimensions.height || + (typeof window !== "undefined" ? window.innerHeight : 0); + // Verificar se está fora dos limites let newX = position.x; let newY = position.y; - + // Ajustar X - garantir que pelo menos 100px fiquem visíveis const minX = -(widgetWidth - 100); const maxX = Math.max(0, winWidth - 100); - + if (newX < minX) { newX = minX; } else if (newX > maxX) { newX = maxX; } - + // Ajustar Y - garantir que pelo menos 100px fiquem visíveis const minY = -(widgetHeight - 100); const maxY = Math.max(0, winHeight - 100); - + if (newY < minY) { newY = minY; } else if (newY > maxY) { newY = maxY; } - + position = { x: newX, y: newY }; - + // Salvar posição após ajuste savePosition(); - + setTimeout(() => { isAnimating = false; }, 300); @@ -721,22 +786,26 @@ // Event listeners globais com cleanup adequado $effect(() => { - if (typeof window === 'undefined') return; - - window.addEventListener('mousemove', handleMouseMove); - window.addEventListener('mouseup', handleMouseUp); - + if (typeof window === "undefined") return; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + return () => { - window.removeEventListener('mousemove', handleMouseMove); - window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); }; }); {#if (!isOpen || isMinimized) && position} - {@const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)} - {@const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)} + {@const winWidth = + windowDimensions.width || + (typeof window !== "undefined" ? window.innerWidth : 0)} + {@const winHeight = + windowDimensions.height || + (typeof window !== "undefined" ? window.innerHeight : 0)} {@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`} {@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`} @@ -939,7 +1039,9 @@ onclick={handleMaximize} aria-label="Maximizar" > -
    +
    - + @@ -963,7 +1067,9 @@ onclick={handleClose} aria-label="Fechar" > -
    +
    - - + +
    @@ -997,8 +1103,8 @@ tabindex="0" aria-label="Redimensionar janela pela borda superior" class="absolute top-0 left-0 right-0 h-2 cursor-ns-resize hover:bg-primary/20 transition-colors z-50" - onmousedown={(e) => handleResizeStart(e, 'n')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'n')} + onmousedown={(e) => handleResizeStart(e, "n")} + onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "n")} style="border-radius: 24px 24px 0 0;" >
    @@ -1007,8 +1113,8 @@ tabindex="0" aria-label="Redimensionar janela pela borda inferior" class="absolute bottom-0 left-0 right-0 h-2 cursor-ns-resize hover:bg-primary/20 transition-colors z-50" - onmousedown={(e) => handleResizeStart(e, 's')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 's')} + onmousedown={(e) => handleResizeStart(e, "s")} + onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "s")} style="border-radius: 0 0 24px 24px;" >
    @@ -1017,8 +1123,8 @@ tabindex="0" aria-label="Redimensionar janela pela borda esquerda" class="absolute top-0 bottom-0 left-0 w-2 cursor-ew-resize hover:bg-primary/20 transition-colors z-50" - onmousedown={(e) => handleResizeStart(e, 'w')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'w')} + onmousedown={(e) => handleResizeStart(e, "w")} + onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "w")} style="border-radius: 24px 0 0 24px;" >
    @@ -1027,8 +1133,8 @@ tabindex="0" aria-label="Redimensionar janela pela borda direita" class="absolute top-0 bottom-0 right-0 w-2 cursor-ew-resize hover:bg-primary/20 transition-colors z-50" - onmousedown={(e) => handleResizeStart(e, 'e')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'e')} + onmousedown={(e) => handleResizeStart(e, "e")} + onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "e")} style="border-radius: 0 24px 24px 0;" >
    @@ -1037,8 +1143,9 @@ tabindex="0" aria-label="Redimensionar janela pelo canto superior esquerdo" class="absolute top-0 left-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/20 transition-colors z-50" - onmousedown={(e) => handleResizeStart(e, 'nw')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'nw')} + onmousedown={(e) => handleResizeStart(e, "nw")} + onkeydown={(e) => + e.key === "Enter" && handleResizeStart(e as any, "nw")} style="border-radius: 24px 0 0 0;" >
    handleResizeStart(e, 'ne')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'ne')} + onmousedown={(e) => handleResizeStart(e, "ne")} + onkeydown={(e) => + e.key === "Enter" && handleResizeStart(e as any, "ne")} style="border-radius: 0 24px 0 0;" >
    handleResizeStart(e, 'sw')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'sw')} + onmousedown={(e) => handleResizeStart(e, "sw")} + onkeydown={(e) => + e.key === "Enter" && handleResizeStart(e as any, "sw")} style="border-radius: 0 0 0 24px;" >
    handleResizeStart(e, 'se')} - onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'se')} + onmousedown={(e) => handleResizeStart(e, "se")} + onkeydown={(e) => + e.key === "Enter" && handleResizeStart(e as any, "se")} style="border-radius: 0 0 24px 0;" >
    @@ -1095,7 +1205,7 @@ } }} onkeydown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); const conversaIdToOpen = notificationMsg?.conversaId; showGlobalNotificationPopup = false; @@ -1111,7 +1221,9 @@ }} >
    -
    +
    - +
    -

    Nova mensagem de {notificationMsg.remetente}

    -

    {notificationMsg.conteudo}

    +

    + Nova mensagem de {notificationMsg.remetente} +

    +

    + {notificationMsg.conteudo} +

    Clique para abrir

    @@ -1152,14 +1283,15 @@ - diff --git a/apps/web/src/lib/components/chat/ChatWindow.svelte b/apps/web/src/lib/components/chat/ChatWindow.svelte index 4988d7c..9cecb9d 100644 --- a/apps/web/src/lib/components/chat/ChatWindow.svelte +++ b/apps/web/src/lib/components/chat/ChatWindow.svelte @@ -10,38 +10,51 @@ import ScheduleMessageModal from "./ScheduleMessageModal.svelte"; import SalaReuniaoManager from "./SalaReuniaoManager.svelte"; import { getAvatarUrl } from "$lib/utils/avatarGenerator"; - import { authStore } from "$lib/stores/auth.svelte"; - import { setupConvexAuth } from "$lib/hooks/convexAuth"; - import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from "lucide-svelte"; + import { + Bell, + X, + ArrowLeft, + LogOut, + MoreVertical, + Users, + Clock, + XCircle, + } from "lucide-svelte"; interface Props { conversaId: string; } let { conversaId }: Props = $props(); - + const client = useConvexClient(); - + // Token é passado automaticamente via interceptadores em +layout.svelte - + let showScheduleModal = $state(false); let showSalaManager = $state(false); let showAdminMenu = $state(false); let showNotificacaoModal = $state(false); const conversas = useQuery(api.chat.listarConversas, {}); - const isAdmin = useQuery(api.chat.verificarSeEhAdmin, { conversaId: conversaId as Id<"conversas"> }); + const isAdmin = useQuery(api.chat.verificarSeEhAdmin, { + conversaId: conversaId as Id<"conversas">, + }); const conversa = $derived(() => { console.log("🔍 [ChatWindow] Buscando conversa ID:", conversaId); console.log("📋 [ChatWindow] Conversas disponíveis:", conversas?.data); - + if (!conversas?.data || !Array.isArray(conversas.data)) { - console.log("⚠️ [ChatWindow] conversas.data não é um array ou está vazio"); + console.log( + "⚠️ [ChatWindow] conversas.data não é um array ou está vazio", + ); return null; } - - const encontrada = conversas.data.find((c: { _id: string }) => c._id === conversaId); + + const encontrada = conversas.data.find( + (c: { _id: string }) => c._id === conversaId, + ); console.log("✅ [ChatWindow] Conversa encontrada:", encontrada); return encontrada; }); @@ -50,7 +63,10 @@ const c = conversa(); if (!c) return "Carregando..."; if (c.tipo === "grupo" || c.tipo === "sala_reuniao") { - return c.nome || (c.tipo === "sala_reuniao" ? "Sala sem nome" : "Grupo sem nome"); + return ( + c.nome || + (c.tipo === "sala_reuniao" ? "Sala sem nome" : "Grupo sem nome") + ); } return c.outroUsuario?.nome || "Usuário"; } @@ -67,10 +83,23 @@ return "👤"; } - function getStatusConversa(): "online" | "offline" | "ausente" | "externo" | "em_reuniao" | null { + function getStatusConversa(): + | "online" + | "offline" + | "ausente" + | "externo" + | "em_reuniao" + | null { const c = conversa(); if (c && c.tipo === "individual" && c.outroUsuario) { - return (c.outroUsuario.statusPresenca as "online" | "offline" | "ausente" | "externo" | "em_reuniao") || "offline"; + return ( + (c.outroUsuario.statusPresenca as + | "online" + | "offline" + | "ausente" + | "externo" + | "em_reuniao") || "offline" + ); } return null; } @@ -86,9 +115,13 @@ async function handleSairGrupoOuSala() { const c = conversa(); if (!c || (c.tipo !== "grupo" && c.tipo !== "sala_reuniao")) return; - + const tipoTexto = c.tipo === "sala_reuniao" ? "sala de reunião" : "grupo"; - if (!confirm(`Tem certeza que deseja sair da ${tipoTexto} "${c.nome || "Sem nome"}"?`)) { + if ( + !confirm( + `Tem certeza que deseja sair da ${tipoTexto} "${c.nome || "Sem nome"}"?`, + ) + ) { return; } @@ -104,7 +137,8 @@ } } catch (error) { console.error("Erro ao sair da conversa:", error); - const errorMessage = error instanceof Error ? error.message : "Erro ao sair da conversa"; + const errorMessage = + error instanceof Error ? error.message : "Erro ao sair da conversa"; alert(errorMessage); } } @@ -112,7 +146,10 @@
    (showAdminMenu = false)}> -
    e.stopPropagation()}> +
    e.stopPropagation()} + > -
    +
    {#if conversa() && conversa()?.tipo === "individual" && conversa()?.outroUsuario}
    -

    {getNomeConversa()}

    +

    + {getNomeConversa()} +

    {#if getStatusMensagem()} -

    {getStatusMensagem()}

    +

    + {getStatusMensagem()} +

    {:else if getStatusConversa()}

    {getStatusConversa() === "online" @@ -169,30 +207,54 @@ {:else if conversa() && (conversa()?.tipo === "grupo" || conversa()?.tipo === "sala_reuniao")}

    - {conversa()?.participantesInfo?.length || 0} {conversa()?.participantesInfo?.length === 1 ? "participante" : "participantes"} + {conversa()?.participantesInfo?.length || 0} + {conversa()?.participantesInfo?.length === 1 + ? "participante" + : "participantes"}

    {#if conversa()?.participantesInfo && conversa()?.participantesInfo.length > 0}
    {#each conversa()?.participantesInfo.slice(0, 5) as participante (participante._id)} -
    +
    {#if participante.fotoPerfilUrl} - {participante.nome} + {participante.nome} {:else if participante.avatar} - {participante.nome} + {participante.nome} {:else} - {participante.nome} + {participante.nome} {/if}
    {/each} {#if conversa()?.participantesInfo.length > 5} -
    +
    +{conversa()?.participantesInfo.length - 5}
    {/if}
    {#if conversa()?.tipo === "sala_reuniao" && isAdmin?.data} - • Admin + • Admin {/if}
    {/if} @@ -215,7 +277,9 @@ aria-label="Sair" title="Sair da conversa" > -
    +
    -
    +
    { e.stopPropagation(); (async () => { - if (!confirm("Tem certeza que deseja encerrar esta reunião? Todos os participantes serão removidos.")) return; + if ( + !confirm( + "Tem certeza que deseja encerrar esta reunião? Todos os participantes serão removidos.", + ) + ) + return; try { - const resultado = await client.mutation(api.chat.encerrarReuniao, { - conversaId: conversaId as Id<"conversas">, - }); + const resultado = await client.mutation( + api.chat.encerrarReuniao, + { + conversaId: conversaId as Id<"conversas">, + }, + ); if (resultado.sucesso) { alert("Reunião encerrada com sucesso!"); voltarParaLista(); @@ -295,7 +369,10 @@ alert(resultado.erro || "Erro ao encerrar reunião"); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Erro ao encerrar reunião"; + const errorMessage = + error instanceof Error + ? error.message + : "Erro ao encerrar reunião"; alert(errorMessage); } showAdminMenu = false; @@ -310,7 +387,7 @@ {/if}
    {/if} - +
    {/if} - diff --git a/apps/web/src/lib/components/chat/MessageInput.svelte b/apps/web/src/lib/components/chat/MessageInput.svelte index 8f35137..258e237 100644 --- a/apps/web/src/lib/components/chat/MessageInput.svelte +++ b/apps/web/src/lib/components/chat/MessageInput.svelte @@ -3,7 +3,6 @@ import { api } from "@sgse-app/backend/convex/_generated/api"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import { onMount } from "svelte"; - import { authStore } from "$lib/stores/auth.svelte"; import { Paperclip, Smile, Send } from "lucide-svelte"; interface Props { @@ -35,18 +34,67 @@ let uploadingFile = $state(false); let digitacaoTimeout: ReturnType | null = null; let showEmojiPicker = $state(false); - let mensagemRespondendo: { id: Id<"mensagens">; conteudo: string; remetente: string } | null = $state(null); + let mensagemRespondendo: { + id: Id<"mensagens">; + conteudo: string; + remetente: string; + } | null = $state(null); let showMentionsDropdown = $state(false); let mentionQuery = $state(""); let mentionStartPos = $state(0); // Emojis mais usados const emojis = [ - "😀", "😃", "😄", "😁", "😅", "😂", "🤣", "😊", "😇", "🙂", - "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", "😋", - "😛", "😝", "😜", "🤪", "🤨", "🧐", "🤓", "😎", "🥳", "😏", - "👍", "👎", "👏", "🙌", "🤝", "🙏", "💪", "✨", "🎉", "🎊", - "❤️", "💙", "💚", "💛", "🧡", "💜", "🖤", "🤍", "💯", "🔥", + "😀", + "😃", + "😄", + "😁", + "😅", + "😂", + "🤣", + "😊", + "😇", + "🙂", + "🙃", + "😉", + "😌", + "😍", + "🥰", + "😘", + "😗", + "😙", + "😚", + "😋", + "😛", + "😝", + "😜", + "🤪", + "🤨", + "🧐", + "🤓", + "😎", + "🥳", + "😏", + "👍", + "👎", + "👏", + "🙌", + "🤝", + "🙏", + "💪", + "✨", + "🎉", + "🎊", + "❤️", + "💙", + "💚", + "💛", + "🧡", + "💜", + "🖤", + "🤍", + "💯", + "🔥", ]; function adicionarEmoji(emoji: string) { @@ -60,7 +108,11 @@ // Obter conversa atual const conversa = $derived((): ConversaComParticipantes | null => { if (!conversas?.data) return null; - return (conversas.data as ConversaComParticipantes[]).find((c) => c._id === conversaId) || null; + return ( + (conversas.data as ConversaComParticipantes[]).find( + (c) => c._id === conversaId, + ) || null + ); }); // Obter participantes para menções (apenas grupos e salas) @@ -74,10 +126,13 @@ const participantesFiltrados = $derived((): ParticipanteInfo[] => { if (!mentionQuery.trim()) return participantesParaMencoes().slice(0, 5); const query = mentionQuery.toLowerCase(); - return participantesParaMencoes().filter((p) => - p.nome?.toLowerCase().includes(query) || - (p.email && p.email.toLowerCase().includes(query)) - ).slice(0, 5); + return participantesParaMencoes() + .filter( + (p) => + p.nome?.toLowerCase().includes(query) || + (p.email && p.email.toLowerCase().includes(query)), + ) + .slice(0, 5); }); // Auto-resize do textarea e detectar menções @@ -91,19 +146,19 @@ // Detectar menções (@) const cursorPos = target.selectionStart || 0; const textBeforeCursor = mensagem.substring(0, cursorPos); - const lastAtIndex = textBeforeCursor.lastIndexOf('@'); - + const lastAtIndex = textBeforeCursor.lastIndexOf("@"); + if (lastAtIndex !== -1) { const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1); // Se não há espaço após o @, mostrar dropdown - if (!textAfterAt.includes(' ') && !textAfterAt.includes('\n')) { + if (!textAfterAt.includes(" ") && !textAfterAt.includes("\n")) { mentionQuery = textAfterAt; mentionStartPos = lastAtIndex; showMentionsDropdown = true; return; } } - + showMentionsDropdown = false; // Indicador de digitação (debounce de 1s) @@ -118,9 +173,11 @@ } function inserirMencao(participante: ParticipanteInfo) { - const nome = participante.nome.split(' ')[0]; // Usar primeiro nome + const nome = participante.nome.split(" ")[0]; // Usar primeiro nome const antes = mensagem.substring(0, mentionStartPos); - const depois = mensagem.substring(textarea.selectionStart || mensagem.length); + const depois = mensagem.substring( + textarea.selectionStart || mensagem.length, + ); mensagem = antes + `@${nome} ` + depois; showMentionsDropdown = false; mentionQuery = ""; @@ -143,8 +200,9 @@ let match; while ((match = mentionRegex.exec(texto)) !== null) { const nomeMencionado = match[1]; - const participante = participantesParaMencoes().find((p) => - p.nome.split(' ')[0].toLowerCase() === nomeMencionado.toLowerCase() + const participante = participantesParaMencoes().find( + (p) => + p.nome.split(" ")[0].toLowerCase() === nomeMencionado.toLowerCase(), ); if (participante) { mencoesIds.push(participante._id); @@ -168,9 +226,12 @@ respostaPara: mensagemRespondendo?.id, mencoes: mencoesIds.length > 0 ? mencoesIds : undefined, }); - - console.log("✅ [MessageInput] Mensagem enviada com sucesso! ID:", result); - + + console.log( + "✅ [MessageInput] Mensagem enviada com sucesso! ID:", + result, + ); + mensagem = ""; mensagemRespondendo = null; showMentionsDropdown = false; @@ -201,17 +262,21 @@ const handler = (e: Event) => { const customEvent = e as CustomEvent<{ mensagemId: Id<"mensagens"> }>; // Buscar informações da mensagem para exibir preview - client.query(api.chat.obterMensagens, { conversaId, limit: 100 }).then((mensagens) => { - const msg = (mensagens as MensagemComRemetente[]).find((m) => m._id === customEvent.detail.mensagemId); - if (msg) { - mensagemRespondendo = { - id: msg._id, - conteudo: msg.conteudo.substring(0, 100), - remetente: msg.remetente?.nome || "Usuário", - }; - textarea?.focus(); - } - }); + client + .query(api.chat.obterMensagens, { conversaId, limit: 100 }) + .then((mensagens) => { + const msg = (mensagens as MensagemComRemetente[]).find( + (m) => m._id === customEvent.detail.mensagemId, + ); + if (msg) { + mensagemRespondendo = { + id: msg._id, + conteudo: msg.conteudo.substring(0, 100), + remetente: msg.remetente?.nome || "Usuário", + }; + textarea?.focus(); + } + }); }; window.addEventListener("responderMensagem", handler); @@ -236,7 +301,7 @@ return; } } - + // Enter sem Shift = enviar if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); @@ -259,7 +324,9 @@ uploadingFile = true; // 1. Obter upload URL - const uploadUrl = await client.mutation(api.chat.uploadArquivoChat, { conversaId }); + const uploadUrl = await client.mutation(api.chat.uploadArquivoChat, { + conversaId, + }); // 2. Upload do arquivo const result = await fetch(uploadUrl, { @@ -275,7 +342,9 @@ const { storageId } = await result.json(); // 3. Enviar mensagem com o arquivo - const tipo: "imagem" | "arquivo" = file.type.startsWith("image/") ? "imagem" : "arquivo"; + const tipo: "imagem" | "arquivo" = file.type.startsWith("image/") + ? "imagem" + : "arquivo"; await client.mutation(api.chat.enviarMensagem, { conversaId, conteudo: tipo === "imagem" ? "" : file.name, @@ -306,10 +375,16 @@
    {#if mensagemRespondendo} -
    +
    -

    Respondendo a {mensagemRespondendo.remetente}

    -

    {mensagemRespondendo.conteudo}

    +

    + Respondendo a {mensagemRespondendo.remetente} +

    +

    + {mensagemRespondendo.conteudo} +

    {/each} @@ -429,15 +520,19 @@
    - diff --git a/apps/web/src/lib/components/chat/MessageList.svelte b/apps/web/src/lib/components/chat/MessageList.svelte index 84e7d8c..2311f8b 100644 --- a/apps/web/src/lib/components/chat/MessageList.svelte +++ b/apps/web/src/lib/components/chat/MessageList.svelte @@ -5,7 +5,6 @@ import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; import { onMount, tick } from "svelte"; - import { authStore } from "$lib/stores/auth.svelte"; interface Props { conversaId: Id<"conversas">; @@ -14,17 +13,25 @@ let { conversaId }: Props = $props(); const client = useConvexClient(); - const mensagens = useQuery(api.chat.obterMensagens, { conversaId, limit: 50 }); + const mensagens = useQuery(api.chat.obterMensagens, { + conversaId, + limit: 50, + }); const digitando = useQuery(api.chat.obterDigitando, { conversaId }); const isAdmin = useQuery(api.chat.verificarSeEhAdmin, { conversaId }); const conversas = useQuery(api.chat.listarConversas, {}); + // Usuário atual + const currentUser = useQuery(api.auth.getCurrentUser, {}); let messagesContainer: HTMLDivElement; let shouldScrollToBottom = true; let lastMessageCount = 0; let mensagensNotificadas = $state>(new Set()); let showNotificationPopup = $state(false); - let notificationMessage = $state<{ remetente: string; conteudo: string } | null>(null); + let notificationMessage = $state<{ + remetente: string; + conteudo: string; + } | null>(null); let notificationTimeout: ReturnType | null = null; let mensagensCarregadas = $state(false); @@ -33,8 +40,8 @@ // Carregar mensagens já notificadas do localStorage ao montar $effect(() => { - if (typeof window !== 'undefined' && !mensagensCarregadas) { - const saved = localStorage.getItem('chat-mensagens-notificadas'); + if (typeof window !== "undefined" && !mensagensCarregadas) { + const saved = localStorage.getItem("chat-mensagens-notificadas"); if (saved) { try { const ids = JSON.parse(saved) as string[]; @@ -44,7 +51,7 @@ } } mensagensCarregadas = true; - + // Marcar todas as mensagens atuais como já visualizadas (não tocar beep ao abrir) if (mensagens?.data && mensagens.data.length > 0) { mensagens.data.forEach((msg) => { @@ -57,17 +64,20 @@ // Salvar mensagens notificadas no localStorage function salvarMensagensNotificadas() { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { const ids = Array.from(mensagensNotificadas); // Limitar a 1000 IDs para não encher o localStorage const idsLimitados = ids.slice(-1000); - localStorage.setItem('chat-mensagens-notificadas', JSON.stringify(idsLimitados)); + localStorage.setItem( + "chat-mensagens-notificadas", + JSON.stringify(idsLimitados), + ); } } - // Atualizar usuarioAtualId sempre que authStore.usuario mudar + // Atualizar usuarioAtualId sempre que currentUser mudar $effect(() => { - const usuario = authStore.usuario; + const usuario = currentUser?.data; if (usuario?._id) { const idStr = String(usuario._id).trim(); usuarioAtualId = idStr || null; @@ -80,11 +90,14 @@ function tocarSomNotificacao() { try { // Usar AudioContext (requer interação do usuário para iniciar) - const AudioContextClass = window.AudioContext || (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; + const AudioContextClass = + window.AudioContext || + (window as { webkitAudioContext?: typeof AudioContext }) + .webkitAudioContext; if (!AudioContextClass) return; - + let audioContext: AudioContext | null = null; - + try { audioContext = new AudioContext(); } catch (e) { @@ -92,40 +105,49 @@ 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 - }); + 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'; - + oscillator.type = "sine"; + gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); - + gainNode.gain.exponentialRampToValueAtTime( + 0.01, + audioContext.currentTime + 0.3, + ); + oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } @@ -140,31 +162,39 @@ 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 mensagemId = String(ultimaMensagem._id); - const remetenteIdStr = ultimaMensagem.remetenteId - ? String(ultimaMensagem.remetenteId).trim() - : (ultimaMensagem.remetente?._id ? String(ultimaMensagem.remetente._id).trim() : null); - + 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) E ainda não foi notificada - if (remetenteIdStr && remetenteIdStr !== usuarioAtualId && !mensagensNotificadas.has(mensagemId)) { + if ( + remetenteIdStr && + remetenteIdStr !== usuarioAtualId && + !mensagensNotificadas.has(mensagemId) + ) { // Marcar como notificada antes de tocar som (evita duplicação) mensagensNotificadas.add(mensagemId); salvarMensagensNotificadas(); - + // Tocar som de notificação (apenas uma vez) tocarSomNotificacao(); - + // Mostrar popup de notificação notificationMessage = { remetente: ultimaMensagem.remetente?.nome || "Usuário", - conteudo: ultimaMensagem.conteudo.substring(0, 100) + (ultimaMensagem.conteudo.length > 100 ? "..." : "") + conteudo: + ultimaMensagem.conteudo.substring(0, 100) + + (ultimaMensagem.conteudo.length > 100 ? "..." : ""), }; showNotificationPopup = true; - + // Ocultar popup após 5 segundos if (notificationTimeout) { clearTimeout(notificationTimeout); @@ -175,7 +205,7 @@ }, 5000); } } - + if (isNewMessage || shouldScrollToBottom) { // Usar requestAnimationFrame para garantir que o DOM foi atualizado requestAnimationFrame(() => { @@ -186,7 +216,7 @@ }); }); } - + lastMessageCount = currentCount; } }); @@ -195,9 +225,11 @@ $effect(() => { if (mensagens?.data && 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); + const remetenteIdStr = ultimaMensagem.remetenteId + ? String(ultimaMensagem.remetenteId).trim() + : ultimaMensagem.remetente?._id + ? String(ultimaMensagem.remetente._id).trim() + : null; // Só marcar como lida se não for minha mensagem if (remetenteIdStr && remetenteIdStr !== usuarioAtualId) { client.mutation(api.chat.marcarComoLida, { @@ -265,7 +297,9 @@ lidaPor?: Id<"usuarios">[]; // IDs dos usuários que leram a mensagem } - function agruparMensagensPorDia(msgs: Mensagem[]): Record { + function agruparMensagensPorDia( + msgs: Mensagem[], + ): Record { const grupos: Record = {}; for (const msg of msgs) { const dia = formatarDiaMensagem(msg.enviadaEm); @@ -291,7 +325,9 @@ }); } - function getEmojisReacao(mensagem: Mensagem): Array<{ emoji: string; count: number }> { + function getEmojisReacao( + mensagem: Mensagem, + ): Array<{ emoji: string; count: number }> { if (!mensagem.reagiuPor || mensagem.reagiuPor.length === 0) return []; const emojiMap: Record = {}; @@ -336,27 +372,33 @@ novoConteudoEditado = ""; } - async function deletarMensagem(mensagemId: Id<"mensagens">, isAdminDeleting: boolean = false) { - const mensagemTexto = isAdminDeleting + async function deletarMensagem( + mensagemId: Id<"mensagens">, + isAdminDeleting: boolean = false, + ) { + const mensagemTexto = isAdminDeleting ? "Tem certeza que deseja deletar esta mensagem como administrador? O remetente será notificado." : "Tem certeza que deseja deletar esta mensagem?"; - + if (!confirm(mensagemTexto)) { return; } try { if (isAdminDeleting) { - const resultado = await client.mutation(api.chat.deletarMensagemComoAdmin, { - mensagemId, - }); + const resultado = await client.mutation( + api.chat.deletarMensagemComoAdmin, + { + mensagemId, + }, + ); if (!resultado.sucesso) { alert(resultado.erro || "Erro ao deletar mensagem"); } } else { - await client.mutation(api.chat.deletarMensagem, { - mensagemId, - }); + await client.mutation(api.chat.deletarMensagem, { + mensagemId, + }); } } catch (error) { console.error("Erro ao deletar mensagem:", error); @@ -393,7 +435,7 @@ // Para conversas individuais: verificar se o outro participante leu if (conversa.tipo === "individual") { const outroParticipante = conversa.participantes?.find( - (p: any) => String(p) !== usuarioAtualId + (p: any) => String(p) !== usuarioAtualId, ); if (outroParticipante) { return lidaPorStr.includes(String(outroParticipante)); @@ -402,13 +444,16 @@ // Para grupos/salas: verificar se pelo menos um outro participante leu if (conversa.tipo === "grupo" || conversa.tipo === "sala_reuniao") { - const outrosParticipantes = conversa.participantes?.filter( - (p: any) => String(p) !== usuarioAtualId && String(p) !== String(mensagem.remetenteId) - ) || []; + const outrosParticipantes = + conversa.participantes?.filter( + (p: any) => + String(p) !== usuarioAtualId && + String(p) !== String(mensagem.remetenteId), + ) || []; if (outrosParticipantes.length === 0) return false; // Verificar se pelo menos um outro participante leu return outrosParticipantes.some((p: any) => - lidaPorStr.includes(String(p)) + lidaPorStr.includes(String(p)), ); } @@ -426,7 +471,9 @@ {#each Object.entries(gruposPorDia) as [dia, mensagensDia]}
    -
    +
    {dia}
    @@ -444,14 +491,17 @@ } return null; })()} - {@const isMinha = usuarioAtualId && remetenteIdStr && remetenteIdStr === usuarioAtualId} -
    -
    + {@const isMinha = + usuarioAtualId && remetenteIdStr && remetenteIdStr === usuarioAtualId} +
    +
    {#if isMinha} -

    - Você -

    +

    Você

    {:else}

    {mensagem.remetente?.nome || "Usuário"} @@ -468,13 +518,15 @@ > {#if mensagem.mensagemOriginal} -

    +

    {mensagem.mensagemOriginal.remetente?.nome || "Usuário"}

    - {mensagem.mensagemOriginal.deletada - ? "Mensagem deletada" + {mensagem.mensagemOriginal.deletada + ? "Mensagem deletada" : mensagem.mensagemOriginal.conteudo}

    @@ -514,12 +566,16 @@ {:else if mensagem.tipo === "texto"} {#if mensagem.conteudo} -

    {mensagem.conteudo}

    +

    + {mensagem.conteudo} +

    {/if} {:else if mensagem.tipo === "arquivo"} handleReagir(mensagem._id, reacao.emoji)} > - {reacao.emoji} {reacao.count} + {reacao.emoji} + {reacao.count} {/each}
    @@ -622,91 +688,91 @@ {/if}
    - -
    -

    - {formatarDataMensagem(mensagem.enviadaEm)} -

    - {#if isMinha && !mensagem.deletada && !mensagem.agendadaPara} - -
    - {#if mensagemFoiLida(mensagem)} - - - - - - - - {:else} - - - - - {/if} -
    - {/if} - {#if !mensagem.deletada && !mensagem.agendadaPara} -
    - {#if isMinha} - - - - {:else if isAdmin?.data} - - - {/if} -
    - {/if} -
    + +
    +

    + {formatarDataMensagem(mensagem.enviadaEm)} +

    + {#if isMinha && !mensagem.deletada && !mensagem.agendadaPara} + +
    + {#if mensagemFoiLida(mensagem)} + + + + + + + + {:else} + + + + + {/if} +
    + {/if} + {#if !mensagem.deletada && !mensagem.agendadaPara} +
    + {#if isMinha} + + + + {:else if isAdmin?.data} + + + {/if} +
    + {/if} +
    {/each} @@ -716,7 +782,9 @@ {#if digitando?.data && digitando.data.length > 0}
    -
    +

    - {digitando.data.map((u: { nome: string }) => u.nome).join(", ")} {digitando.data.length === 1 + {digitando.data.map((u: { nome: string }) => u.nome).join(", ")} + {digitando.data.length === 1 ? "está digitando" : "estão digitando"}...

    @@ -756,7 +825,9 @@ />

    Nenhuma mensagem ainda

    -

    Envie a primeira mensagem!

    +

    + Envie a primeira mensagem! +

    {/if}
    @@ -775,7 +846,9 @@ }} >
    -
    +
    - +
    -

    Nova mensagem de {notificationMessage.remetente}

    -

    {notificationMessage.conteudo}

    +

    + Nova mensagem de {notificationMessage.remetente} +

    +

    + {notificationMessage.conteudo} +

    {/if} - diff --git a/apps/web/src/lib/components/chat/NewConversationModal.svelte b/apps/web/src/lib/components/chat/NewConversationModal.svelte index 53b9d1c..3489113 100644 --- a/apps/web/src/lib/components/chat/NewConversationModal.svelte +++ b/apps/web/src/lib/components/chat/NewConversationModal.svelte @@ -2,10 +2,19 @@ import { useQuery, useConvexClient } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; import { abrirConversa } from "$lib/stores/chatStore"; - import { authStore } from "$lib/stores/auth.svelte"; import UserStatusBadge from "./UserStatusBadge.svelte"; import UserAvatar from "./UserAvatar.svelte"; - import { MessageSquare, User, Users, Video, X, Search, ChevronRight, Plus, UserX } from "lucide-svelte"; + import { + MessageSquare, + User, + Users, + Video, + X, + Search, + ChevronRight, + Plus, + UserX, + } from "lucide-svelte"; interface Props { onClose: () => void; @@ -16,6 +25,8 @@ const client = useConvexClient(); const usuarios = useQuery(api.usuarios.listarParaChat, {}); const meuPerfil = useQuery(api.usuarios.obterPerfil, {}); + // Usuário atual + const currentUser = useQuery(api.auth.getCurrentUser, {}); let activeTab = $state<"individual" | "grupo" | "sala_reuniao">("individual"); let searchQuery = $state(""); @@ -26,27 +37,36 @@ const usuariosFiltrados = $derived(() => { if (!usuarios?.data) return []; - + // Filtrar o próprio usuário - const meuId = authStore.usuario?._id || meuPerfil?.data?._id; + const meuId = currentUser?.data?._id || meuPerfil?.data?._id; let lista = usuarios.data.filter((u: any) => u._id !== meuId); - + // Aplicar busca if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); - lista = lista.filter((u: any) => - u.nome?.toLowerCase().includes(query) || - u.email?.toLowerCase().includes(query) || - u.matricula?.toLowerCase().includes(query) + lista = lista.filter( + (u: any) => + u.nome?.toLowerCase().includes(query) || + u.email?.toLowerCase().includes(query) || + u.matricula?.toLowerCase().includes(query), ); } - + // Ordenar: online primeiro, depois por nome return lista.sort((a: any, b: any) => { - const statusOrder = { online: 0, ausente: 1, externo: 2, em_reuniao: 3, offline: 4 }; - const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4; - const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4; - + const statusOrder = { + online: 0, + ausente: 1, + externo: 2, + em_reuniao: 3, + offline: 4, + }; + const statusA = + statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4; + const statusB = + statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4; + if (statusA !== statusB) return statusA - statusB; return (a.nome || "").localeCompare(b.nome || ""); }); @@ -99,7 +119,8 @@ onClose(); } catch (error: any) { console.error("Erro ao criar grupo:", error); - const mensagem = error?.message || error?.data || "Erro desconhecido ao criar grupo"; + const mensagem = + error?.message || error?.data || "Erro desconhecido ao criar grupo"; alert(`Erro ao criar grupo: ${mensagem}`); } finally { loading = false; @@ -127,7 +148,10 @@ onClose(); } catch (error: any) { console.error("Erro ao criar sala de reunião:", error); - const mensagem = error?.message || error?.data || "Erro desconhecido ao criar sala de reunião"; + const mensagem = + error?.message || + error?.data || + "Erro desconhecido ao criar sala de reunião"; alert(`Erro ao criar sala de reunião: ${mensagem}`); } finally { loading = false; @@ -135,10 +159,18 @@ } - e.target === e.currentTarget && onClose()}> -