|
|
|
|
@@ -18,6 +18,9 @@
|
|
|
|
|
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
|
|
|
|
|
|
|
|
|
|
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
|
|
|
|
|
|
|
|
|
|
// Query para verificar o ID do usuário logado (usar como referência)
|
|
|
|
|
const meuPerfilQuery = useQuery(api.usuarios.obterPerfil, {});
|
|
|
|
|
|
|
|
|
|
let isOpen = $state(false);
|
|
|
|
|
let isMinimized = $state(false);
|
|
|
|
|
@@ -40,12 +43,14 @@
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Posição do widget (arrastável)
|
|
|
|
|
let position = $state({ x: 0, y: 0 });
|
|
|
|
|
// Inicializar posição como null para indicar que precisa ser calculada
|
|
|
|
|
let position = $state<{ x: number; y: number } | null>(null);
|
|
|
|
|
let isDragging = $state(false);
|
|
|
|
|
let dragStart = $state({ x: 0, y: 0 });
|
|
|
|
|
let isAnimating = $state(false);
|
|
|
|
|
let dragThreshold = $state(5); // Distância mínima em pixels para considerar arrastar
|
|
|
|
|
let hasMoved = $state(false); // Flag para verificar se houve movimento durante o arrastar
|
|
|
|
|
let shouldPreventClick = $state(false); // Flag para prevenir clique após arrastar
|
|
|
|
|
|
|
|
|
|
// Tamanho da janela (redimensionável)
|
|
|
|
|
const MIN_WIDTH = 300;
|
|
|
|
|
@@ -76,7 +81,7 @@
|
|
|
|
|
let windowSize = $state(getSavedSize());
|
|
|
|
|
let isMaximized = $state(false);
|
|
|
|
|
let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
|
|
|
|
|
let previousPosition = $state({ x: 0, y: 0 });
|
|
|
|
|
let previousPosition = $state<{ x: number; y: number } | null>(null);
|
|
|
|
|
|
|
|
|
|
// Dimensões da janela (reativo)
|
|
|
|
|
let windowDimensions = $state({ width: 0, height: 0 });
|
|
|
|
|
@@ -97,10 +102,36 @@
|
|
|
|
|
|
|
|
|
|
updateWindowDimensions();
|
|
|
|
|
|
|
|
|
|
// Inicializar posição apenas uma vez quando as dimensões estiverem disponíveis
|
|
|
|
|
if (position === null) {
|
|
|
|
|
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 = {
|
|
|
|
|
x: window.innerWidth - 72 - 24,
|
|
|
|
|
y: window.innerHeight - 72 - 24
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Posição padrão: canto inferior direito
|
|
|
|
|
position = {
|
|
|
|
|
x: window.innerWidth - 72 - 24,
|
|
|
|
|
y: window.innerHeight - 72 - 24
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
savePosition(); // Salvar posição inicial
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
updateWindowDimensions();
|
|
|
|
|
// Ajustar posição quando a janela redimensionar
|
|
|
|
|
ajustarPosicao();
|
|
|
|
|
if (position) {
|
|
|
|
|
ajustarPosicao();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
@@ -109,6 +140,13 @@
|
|
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Salvar posição no localStorage
|
|
|
|
|
function savePosition() {
|
|
|
|
|
if (typeof window !== 'undefined' && position) {
|
|
|
|
|
localStorage.setItem('chat-widget-position', JSON.stringify(position));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Salvar tamanho no localStorage
|
|
|
|
|
function saveSize() {
|
|
|
|
|
@@ -138,7 +176,7 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleResizeMove(e: MouseEvent) {
|
|
|
|
|
if (!isResizing || !resizeDirection) return;
|
|
|
|
|
if (!isResizing || !resizeDirection || !position) return;
|
|
|
|
|
|
|
|
|
|
const deltaX = e.clientX - resizeStart.x;
|
|
|
|
|
const deltaY = e.clientY - resizeStart.y;
|
|
|
|
|
@@ -193,6 +231,63 @@
|
|
|
|
|
|
|
|
|
|
$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) {
|
|
|
|
|
const conversas = todasConversas.data as ConversaComTimestamp[];
|
|
|
|
|
const conversaAberta = conversas.find((c) => String(c._id) === String(activeConversation));
|
|
|
|
|
|
|
|
|
|
if (conversaAberta && conversaAberta.ultimaMensagemTimestamp) {
|
|
|
|
|
const mensagemId = `${conversaAberta._id}-${conversaAberta.ultimaMensagemTimestamp}`;
|
|
|
|
|
if (!mensagensNotificadasGlobal.has(mensagemId)) {
|
|
|
|
|
mensagensNotificadasGlobal.add(mensagemId);
|
|
|
|
|
salvarMensagensNotificadasGlobal();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Ajustar posição quando a janela é aberta pela primeira vez
|
|
|
|
|
let wasPreviouslyClosed = $state(true);
|
|
|
|
|
$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 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
|
|
|
|
|
// Ajustar para manter aproximadamente a mesma distância do canto inferior
|
|
|
|
|
if (position.y > maxY || position.y < minY) {
|
|
|
|
|
// 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 };
|
|
|
|
|
savePosition();
|
|
|
|
|
// Forçar ajuste imediatamente
|
|
|
|
|
ajustarPosicao();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wasPreviouslyClosed = false;
|
|
|
|
|
} else if (!isOpen || isMinimized) {
|
|
|
|
|
wasPreviouslyClosed = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Tipos para conversas
|
|
|
|
|
@@ -294,15 +389,73 @@
|
|
|
|
|
const conversas = todasConversas.data as ConversaComTimestamp[];
|
|
|
|
|
|
|
|
|
|
// Encontrar conversas com novas mensagens
|
|
|
|
|
const meuId = String(authStore.usuario._id);
|
|
|
|
|
// Obter ID do usuário logado de forma robusta
|
|
|
|
|
// Prioridade: usar query do Convex (mais confiável) > authStore
|
|
|
|
|
const usuarioLogado = authStore.usuario;
|
|
|
|
|
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();
|
|
|
|
|
} else if (usuarioLogado && usuarioLogado._id) {
|
|
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
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",
|
|
|
|
|
nome: usuarioLogado?.nome || perfilConvex?.nome,
|
|
|
|
|
email: usuarioLogado?.email
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conversas.forEach((conv) => {
|
|
|
|
|
if (!conv.ultimaMensagemTimestamp) return;
|
|
|
|
|
|
|
|
|
|
// Verificar se a última mensagem foi enviada pelo usuário atual
|
|
|
|
|
const remetenteIdStr = conv.ultimaMensagemRemetenteId ? String(conv.ultimaMensagemRemetenteId) : null;
|
|
|
|
|
// Comparação mais robusta: normalizar ambos os IDs para string e comparar
|
|
|
|
|
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)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
// Marcar como notificada para evitar processamento futuro
|
|
|
|
|
const mensagemId = `${conv._id}-${conv.ultimaMensagemTimestamp}`;
|
|
|
|
|
if (!mensagensNotificadasGlobal.has(mensagemId)) {
|
|
|
|
|
mensagensNotificadasGlobal.add(mensagemId);
|
|
|
|
|
salvarMensagensNotificadasGlobal();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -312,12 +465,15 @@
|
|
|
|
|
// Verificar se já foi notificada
|
|
|
|
|
if (mensagensNotificadasGlobal.has(mensagemId)) return;
|
|
|
|
|
|
|
|
|
|
const conversaAtivaId = activeConversation ? String(activeConversation) : null;
|
|
|
|
|
const conversaIdStr = String(conv._id);
|
|
|
|
|
const conversaAtivaId = activeConversation ? String(activeConversation).trim() : null;
|
|
|
|
|
const conversaIdStr = String(conv._id).trim();
|
|
|
|
|
const estaConversaEstaAberta = conversaAtivaId === conversaIdStr;
|
|
|
|
|
|
|
|
|
|
// Só mostrar notificação se não estamos vendo essa conversa
|
|
|
|
|
if (!isOpen || conversaAtivaId !== conversaIdStr) {
|
|
|
|
|
// Marcar como notificada antes de tocar som (evita duplicação)
|
|
|
|
|
// 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
|
|
|
|
|
if (!isOpen || !estaConversaEstaAberta) {
|
|
|
|
|
// Marcar como notificada ANTES de mostrar notificação (evita duplicação)
|
|
|
|
|
mensagensNotificadasGlobal.add(mensagemId);
|
|
|
|
|
salvarMensagensNotificadasGlobal();
|
|
|
|
|
|
|
|
|
|
@@ -340,6 +496,11 @@
|
|
|
|
|
showGlobalNotificationPopup = false;
|
|
|
|
|
globalNotificationMessage = null;
|
|
|
|
|
}, 5000);
|
|
|
|
|
} else {
|
|
|
|
|
// Chat está aberto e estamos vendo essa conversa - marcar como visualizada
|
|
|
|
|
// mas não mostrar notificação nem tocar beep
|
|
|
|
|
mensagensNotificadasGlobal.add(mensagemId);
|
|
|
|
|
salvarMensagensNotificadasGlobal();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@@ -362,10 +523,14 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleMaximize() {
|
|
|
|
|
if (!position) return;
|
|
|
|
|
|
|
|
|
|
if (isMaximized) {
|
|
|
|
|
// Restaurar tamanho anterior
|
|
|
|
|
windowSize = previousSize;
|
|
|
|
|
position = previousPosition;
|
|
|
|
|
if (previousPosition) {
|
|
|
|
|
position = previousPosition;
|
|
|
|
|
}
|
|
|
|
|
isMaximized = false;
|
|
|
|
|
saveSize();
|
|
|
|
|
ajustarPosicao();
|
|
|
|
|
@@ -395,27 +560,36 @@
|
|
|
|
|
|
|
|
|
|
// Funcionalidade de arrastar
|
|
|
|
|
function handleMouseDown(e: MouseEvent) {
|
|
|
|
|
if (e.button !== 0) return; // Apenas botão esquerdo
|
|
|
|
|
if (e.button !== 0 || !position) return; // Apenas botão esquerdo
|
|
|
|
|
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');
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handler específico para o botão flutuante (evita conflito com clique)
|
|
|
|
|
function handleButtonMouseDown(e: MouseEvent) {
|
|
|
|
|
if (e.button !== 0) return;
|
|
|
|
|
// Resetar flag de movimento
|
|
|
|
|
if (e.button !== 0 || !position) return;
|
|
|
|
|
// Resetar flags de movimento e clique
|
|
|
|
|
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');
|
|
|
|
|
// Não prevenir default para permitir clique funcionar se não houver movimento
|
|
|
|
|
}
|
|
|
|
|
@@ -426,45 +600,52 @@
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isDragging) 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
|
|
|
|
|
// Verificar se houve movimento significativo desde o último frame
|
|
|
|
|
const deltaX = Math.abs(newX - position.x);
|
|
|
|
|
const deltaY = Math.abs(newY - position.y);
|
|
|
|
|
if (deltaX > dragThreshold || deltaY > dragThreshold) {
|
|
|
|
|
hasMoved = true;
|
|
|
|
|
|
|
|
|
|
// Se houve qualquer movimento (mesmo pequeno), marcar como movido
|
|
|
|
|
if (deltaX > 0 || deltaY > 0) {
|
|
|
|
|
// Marcar como movido se passar do threshold
|
|
|
|
|
if (deltaX > dragThreshold || deltaY > dragThreshold) {
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// 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)),
|
|
|
|
|
y: Math.max(minY, Math.min(newY, maxY)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
position = {
|
|
|
|
|
x: Math.max(minX, Math.min(newX, maxX)),
|
|
|
|
|
y: Math.max(minY, Math.min(newY, maxY)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleMouseUp(e?: MouseEvent) {
|
|
|
|
|
const hadMoved = hasMoved;
|
|
|
|
|
const shouldPrevent = shouldPreventClick;
|
|
|
|
|
|
|
|
|
|
if (isDragging) {
|
|
|
|
|
isDragging = false;
|
|
|
|
|
hasMoved = false;
|
|
|
|
|
document.body.classList.remove('dragging');
|
|
|
|
|
|
|
|
|
|
// Se estava arrastando e houve movimento, prevenir clique
|
|
|
|
|
if (hadMoved && e) {
|
|
|
|
|
@@ -474,6 +655,17 @@
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
handleResizeEnd();
|
|
|
|
|
|
|
|
|
|
@@ -481,6 +673,8 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ajustarPosicao() {
|
|
|
|
|
if (!position) return;
|
|
|
|
|
|
|
|
|
|
isAnimating = true;
|
|
|
|
|
|
|
|
|
|
// Dimensões do widget
|
|
|
|
|
@@ -517,6 +711,9 @@
|
|
|
|
|
|
|
|
|
|
position = { x: newX, y: newY };
|
|
|
|
|
|
|
|
|
|
// Salvar posição após ajuste
|
|
|
|
|
savePosition();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isAnimating = false;
|
|
|
|
|
}, 300);
|
|
|
|
|
@@ -537,11 +734,11 @@
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
|
|
|
|
{#if !isOpen || isMinimized}
|
|
|
|
|
{#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 bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - 72)}px`}
|
|
|
|
|
{@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - 72)}px`}
|
|
|
|
|
{@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`}
|
|
|
|
|
{@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="fixed group relative border-0 backdrop-blur-xl"
|
|
|
|
|
@@ -564,22 +761,17 @@
|
|
|
|
|
"
|
|
|
|
|
onmousedown={handleButtonMouseDown}
|
|
|
|
|
onmouseup={(e) => {
|
|
|
|
|
const hadMovedBefore = hasMoved;
|
|
|
|
|
handleMouseUp(e);
|
|
|
|
|
// Se houve movimento, prevenir o clique
|
|
|
|
|
if (hadMovedBefore) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onclick={(e) => {
|
|
|
|
|
// Só executar toggle se não houve movimento
|
|
|
|
|
if (!hasMoved) {
|
|
|
|
|
// Só executar toggle se não houve movimento durante o arrastar
|
|
|
|
|
if (!shouldPreventClick && !hasMoved) {
|
|
|
|
|
handleToggle();
|
|
|
|
|
} else {
|
|
|
|
|
// Prevenir clique se houve movimento
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
shouldPreventClick = false; // Resetar após prevenir
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
aria-label="Abrir chat"
|
|
|
|
|
@@ -638,11 +830,11 @@
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
|
|
|
|
|
{#if isOpen && !isMinimized}
|
|
|
|
|
{#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 bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
|
|
|
|
|
{@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
|
|
|
|
|
{@const bottomPos = `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
|
|
|
|
|
{@const rightPos = `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
|
|
|
|
|
<div
|
|
|
|
|
class="fixed flex flex-col overflow-hidden backdrop-blur-2xl"
|
|
|
|
|
style="
|
|
|
|
|
@@ -801,47 +993,79 @@
|
|
|
|
|
<!-- Resize Handles -->
|
|
|
|
|
<!-- Top -->
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
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')}
|
|
|
|
|
style="border-radius: 24px 24px 0 0;"
|
|
|
|
|
></div>
|
|
|
|
|
<!-- Bottom -->
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
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')}
|
|
|
|
|
style="border-radius: 0 0 24px 24px;"
|
|
|
|
|
></div>
|
|
|
|
|
<!-- Left -->
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
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')}
|
|
|
|
|
style="border-radius: 24px 0 0 24px;"
|
|
|
|
|
></div>
|
|
|
|
|
<!-- Right -->
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
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')}
|
|
|
|
|
style="border-radius: 0 24px 24px 0;"
|
|
|
|
|
></div>
|
|
|
|
|
<!-- Corners -->
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
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')}
|
|
|
|
|
style="border-radius: 24px 0 0 0;"
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
aria-label="Redimensionar janela pelo canto superior direito"
|
|
|
|
|
class="absolute top-0 right-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/20 transition-colors z-50"
|
|
|
|
|
onmousedown={(e) => handleResizeStart(e, 'ne')}
|
|
|
|
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'ne')}
|
|
|
|
|
style="border-radius: 0 24px 0 0;"
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
aria-label="Redimensionar janela pelo canto inferior esquerdo"
|
|
|
|
|
class="absolute bottom-0 left-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/20 transition-colors z-50"
|
|
|
|
|
onmousedown={(e) => handleResizeStart(e, 'sw')}
|
|
|
|
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'sw')}
|
|
|
|
|
style="border-radius: 0 0 0 24px;"
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
aria-label="Redimensionar janela pelo canto inferior direito"
|
|
|
|
|
class="absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/20 transition-colors z-50"
|
|
|
|
|
onmousedown={(e) => handleResizeStart(e, 'se')}
|
|
|
|
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'se')}
|
|
|
|
|
style="border-radius: 0 0 24px 0;"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -850,18 +1074,40 @@
|
|
|
|
|
|
|
|
|
|
<!-- Popup Global de Notificação de Nova Mensagem (quando chat está fechado/minimizado) -->
|
|
|
|
|
{#if showGlobalNotificationPopup && globalNotificationMessage}
|
|
|
|
|
{@const notificationMsg = globalNotificationMessage}
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}"
|
|
|
|
|
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={() => {
|
|
|
|
|
const conversaIdToOpen = notificationMsg?.conversaId;
|
|
|
|
|
showGlobalNotificationPopup = false;
|
|
|
|
|
globalNotificationMessage = null;
|
|
|
|
|
if (globalNotificationTimeout) {
|
|
|
|
|
clearTimeout(globalNotificationTimeout);
|
|
|
|
|
}
|
|
|
|
|
// Abrir chat e conversa ao clicar
|
|
|
|
|
abrirChat();
|
|
|
|
|
abrirConversa(globalNotificationMessage.conversaId as Id<"conversas">);
|
|
|
|
|
if (conversaIdToOpen) {
|
|
|
|
|
abrirChat();
|
|
|
|
|
abrirConversa(conversaIdToOpen as Id<"conversas">);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onkeydown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const conversaIdToOpen = notificationMsg?.conversaId;
|
|
|
|
|
showGlobalNotificationPopup = false;
|
|
|
|
|
globalNotificationMessage = null;
|
|
|
|
|
if (globalNotificationTimeout) {
|
|
|
|
|
clearTimeout(globalNotificationTimeout);
|
|
|
|
|
}
|
|
|
|
|
if (conversaIdToOpen) {
|
|
|
|
|
abrirChat();
|
|
|
|
|
abrirConversa(conversaIdToOpen as Id<"conversas">);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
@@ -878,12 +1124,13 @@
|
|
|
|
|
</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="font-semibold text-base-content text-sm mb-1">Nova mensagem de {notificationMsg.remetente}</p>
|
|
|
|
|
<p class="text-xs text-base-content/70 line-clamp-2">{notificationMsg.conteudo}</p>
|
|
|
|
|
<p class="text-xs text-primary mt-1">Clique para abrir</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label="Fechar notificação"
|
|
|
|
|
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();
|
|
|
|
|
|