diff --git a/apps/web/src/lib/components/chat/ChatWindow.svelte b/apps/web/src/lib/components/chat/ChatWindow.svelte
index 9706eb7..14636f6 100644
--- a/apps/web/src/lib/components/chat/ChatWindow.svelte
+++ b/apps/web/src/lib/components/chat/ChatWindow.svelte
@@ -11,6 +11,7 @@
import SalaReuniaoManager from "./SalaReuniaoManager.svelte";
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
import { authStore } from "$lib/stores/auth.svelte";
+ import { Bell, X } from "lucide-svelte";
interface Props {
conversaId: string;
@@ -122,8 +123,8 @@
fill="none"
stroke="currentColor"
stroke-width="2.5"
- stroke-linecap="round"
- stroke-linejoin="round"
+ stroke-linecap="round"
+ stroke-linejoin="round"
class="w-6 h-6 text-primary"
>
@@ -391,21 +392,19 @@
{#if showNotificacaoModal && conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
-
(showNotificacaoModal = false)}>
-
e.stopPropagation()}
- >
+
+
+
{/if}
diff --git a/apps/web/src/lib/components/chat/MessageInput.svelte b/apps/web/src/lib/components/chat/MessageInput.svelte
index 99834b5..4878c6d 100644
--- a/apps/web/src/lib/components/chat/MessageInput.svelte
+++ b/apps/web/src/lib/components/chat/MessageInput.svelte
@@ -9,6 +9,20 @@
conversaId: Id<"conversas">;
}
+ type ParticipanteInfo = {
+ _id: Id<"usuarios">;
+ nome: string;
+ email?: string;
+ fotoPerfilUrl?: string;
+ avatar?: string;
+ };
+
+ type ConversaComParticipantes = {
+ _id: Id<"conversas">;
+ tipo: "individual" | "grupo" | "sala_reuniao";
+ participantesInfo?: ParticipanteInfo[];
+ };
+
let { conversaId }: Props = $props();
const client = useConvexClient();
@@ -43,25 +57,25 @@
}
// Obter conversa atual
- const conversa = $derived(() => {
+ const conversa = $derived((): ConversaComParticipantes | null => {
if (!conversas?.data) return null;
- return conversas.data.find((c: any) => c._id === conversaId);
+ return (conversas.data as ConversaComParticipantes[]).find((c) => c._id === conversaId) || null;
});
// Obter participantes para menções (apenas grupos e salas)
- const participantesParaMencoes = $derived(() => {
+ const participantesParaMencoes = $derived((): ParticipanteInfo[] => {
const c = conversa();
if (!c || (c.tipo !== "grupo" && c.tipo !== "sala_reuniao")) return [];
return c.participantesInfo || [];
});
// Filtrar participantes para dropdown de menções
- const participantesFiltrados = $derived(() => {
+ const participantesFiltrados = $derived((): ParticipanteInfo[] => {
if (!mentionQuery.trim()) return participantesParaMencoes().slice(0, 5);
const query = mentionQuery.toLowerCase();
- return participantesParaMencoes().filter((p: any) =>
+ return participantesParaMencoes().filter((p) =>
p.nome?.toLowerCase().includes(query) ||
- p.email?.toLowerCase().includes(query)
+ (p.email && p.email.toLowerCase().includes(query))
).slice(0, 5);
});
@@ -102,7 +116,7 @@
}, 1000);
}
- function inserirMencao(participante: any) {
+ function inserirMencao(participante: ParticipanteInfo) {
const nome = participante.nome.split(' ')[0]; // Usar primeiro nome
const antes = mensagem.substring(0, mentionStartPos);
const depois = mensagem.substring(textarea.selectionStart || mensagem.length);
@@ -128,7 +142,7 @@
let match;
while ((match = mentionRegex.exec(texto)) !== null) {
const nomeMencionado = match[1];
- const participante = participantesParaMencoes().find((p: any) =>
+ const participante = participantesParaMencoes().find((p) =>
p.nome.split(' ')[0].toLowerCase() === nomeMencionado.toLowerCase()
);
if (participante) {
@@ -175,13 +189,19 @@
mensagemRespondendo = null;
}
+ type MensagemComRemetente = {
+ _id: Id<"mensagens">;
+ conteudo: string;
+ remetente?: { nome: string } | null;
+ };
+
// Escutar evento de resposta
onMount(() => {
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.find((m: any) => m._id === customEvent.detail.mensagemId);
+ const msg = (mensagens as MensagemComRemetente[]).find((m) => m._id === customEvent.detail.mensagemId);
if (msg) {
mensagemRespondendo = {
id: msg._id,
@@ -254,11 +274,11 @@
const { storageId } = await result.json();
// 3. Enviar mensagem com o arquivo
- const tipo = 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,
- tipo: tipo as any,
+ tipo,
arquivoId: storageId,
arquivoNome: file.name,
arquivoTamanho: file.size,
diff --git a/apps/web/src/lib/components/chat/MessageList.svelte b/apps/web/src/lib/components/chat/MessageList.svelte
index e850e61..44b056b 100644
--- a/apps/web/src/lib/components/chat/MessageList.svelte
+++ b/apps/web/src/lib/components/chat/MessageList.svelte
@@ -21,14 +21,49 @@
let messagesContainer: HTMLDivElement;
let shouldScrollToBottom = true;
let lastMessageCount = 0;
- let lastMessageId = $state
(null);
+ let mensagensNotificadas = $state>(new Set());
let showNotificationPopup = $state(false);
let notificationMessage = $state<{ remetente: string; conteudo: string } | null>(null);
let notificationTimeout: ReturnType | null = null;
+ let mensagensCarregadas = $state(false);
// Obter ID do usuário atual - usar $state para garantir reatividade
let usuarioAtualId = $state(null);
+ // Carregar mensagens já notificadas do localStorage ao montar
+ $effect(() => {
+ if (typeof window !== 'undefined' && !mensagensCarregadas) {
+ const saved = localStorage.getItem('chat-mensagens-notificadas');
+ if (saved) {
+ try {
+ const ids = JSON.parse(saved) as string[];
+ mensagensNotificadas = new Set(ids);
+ } catch {
+ mensagensNotificadas = new Set();
+ }
+ }
+ 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) => {
+ mensagensNotificadas.add(String(msg._id));
+ });
+ salvarMensagensNotificadas();
+ }
+ }
+ });
+
+ // Salvar mensagens notificadas no localStorage
+ function salvarMensagensNotificadas() {
+ 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));
+ }
+ }
+
// Atualizar usuarioAtualId sempre que authStore.usuario mudar
$effect(() => {
const usuario = authStore.usuario;
@@ -44,7 +79,9 @@
function tocarSomNotificacao() {
try {
// Usar AudioContext (requer interação do usuário para iniciar)
- const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
+ const AudioContextClass = window.AudioContext || (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
+ if (!AudioContextClass) return;
+
let audioContext: AudioContext | null = null;
try {
@@ -106,13 +143,18 @@
// 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);
- // Se é uma nova mensagem de outro usuário (não minha)
- if (remetenteIdStr && remetenteIdStr !== usuarioAtualId && ultimaMensagem._id !== lastMessageId) {
- // Tocar som de notificação
+ // Se é uma nova mensagem de outro usuário (não minha) E ainda não foi notificada
+ 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
@@ -130,8 +172,6 @@
showNotificationPopup = false;
notificationMessage = null;
}, 5000);
-
- lastMessageId = ultimaMensagem._id;
}
}
@@ -311,11 +351,11 @@
alert(resultado.erro || "Erro ao deletar mensagem");
}
} else {
- await client.mutation(api.chat.deletarMensagem, {
- mensagemId,
- });
+ await client.mutation(api.chat.deletarMensagem, {
+ mensagemId,
+ });
}
- } catch (error: any) {
+ } catch (error) {
console.error("Erro ao deletar mensagem:", error);
alert(error.message || "Erro ao deletar mensagem");
}
@@ -548,20 +588,20 @@
{#if isMinha}
-
-
+
+ title="Deletar mensagem"
+ >
+ 🗑️
+
{:else if isAdmin?.data}
+
+
diff --git a/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte b/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte
index 104efb9..56b776f 100644
--- a/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte
+++ b/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte
@@ -4,6 +4,7 @@
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import UserAvatar from "./UserAvatar.svelte";
import UserStatusBadge from "./UserStatusBadge.svelte";
+ import { X, Users, UserPlus, ArrowUp, ArrowDown, Trash2, Search } from "lucide-svelte";
interface Props {
conversaId: Id<"conversas">;
@@ -151,19 +152,15 @@
}
- {
- if (e.target === e.currentTarget) {
- onClose();
- }
-}}>
-
e.stopPropagation()}
- >
+
{:else if activeTab === "adicionar" && isAdmin}
-
+
+
@@ -356,7 +349,7 @@
{#if isLoading}
{:else}
- +
+
{/if}
{/each}
@@ -376,5 +369,8 @@
-
+
+
diff --git a/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte b/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte
index 7ed6791..03dbf74 100644
--- a/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte
+++ b/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte
@@ -99,70 +99,21 @@
}
-
-
- e.key === 'Escape' && onClose()}
->
-
-
e.stopPropagation()}
- role="dialog"
- aria-modal="true"
- aria-labelledby="modal-title"
- tabindex="-1"
- >
-
-
-
-
-
-
-
-
- Agendar Mensagem
+
-
-
-
+
+
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index e13ff68..e43a543 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -4,4 +4,7 @@ import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
+ resolve: {
+ dedupe: ["lucide-svelte"],
+ },
});
diff --git a/bun.lock b/bun.lock
index 16a9a59..fca6f51 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,7 +6,7 @@
"dependencies": {
"@tanstack/svelte-form": "^1.23.8",
"chart.js": "^4.5.1",
- "lucide-svelte": "^0.548.0",
+ "lucide-svelte": "^0.552.0",
"svelte-chartjs": "^3.1.5",
"svelte-sonner": "^1.0.5",
},
@@ -61,7 +61,7 @@
"version": "1.0.0",
"dependencies": {
"@dicebear/avataaars": "^9.2.4",
- "convex": "^1.17.4",
+ "convex": "catalog:",
"nodemailer": "^7.0.10",
},
"devDependencies": {
@@ -624,7 +624,7 @@
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
- "lucide-svelte": ["lucide-svelte@0.548.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-aW2BfHWBLWf/XPSKytTPV16AWfFeFIJeUyOg7eHY2rhzVQ0u0LIvoS4pm2oskr+OJVw+NsS8fPvlBVqPfUO1XQ=="],
+ "lucide-svelte": ["lucide-svelte@0.552.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-zynJ64KOsuQG3I4tSqfvvl7Kc9x4mWkppbxsuyrbegQwma9HFhBp4aE6HuQNF4c3pS0AHWHki5CAMs5m3QXA5w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
diff --git a/package.json b/package.json
index f8b266d..9088df1 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"dependencies": {
"@tanstack/svelte-form": "^1.23.8",
"chart.js": "^4.5.1",
- "lucide-svelte": "^0.548.0",
+ "lucide-svelte": "^0.552.0",
"svelte-chartjs": "^3.1.5",
"svelte-sonner": "^1.0.5"
},
diff --git a/packages/backend/convex/chat.ts b/packages/backend/convex/chat.ts
index 28978fe..127b3bf 100644
--- a/packages/backend/convex/chat.ts
+++ b/packages/backend/convex/chat.ts
@@ -369,6 +369,7 @@ export const enviarMensagem = mutation({
await ctx.db.patch(args.conversaId, {
ultimaMensagem: args.conteudo.substring(0, 100),
ultimaMensagemTimestamp: Date.now(),
+ ultimaMensagemRemetenteId: usuarioAtual._id, // Guardar ID do remetente da última mensagem
});
// Criar notificações para participantes (com tratamento de erro)
@@ -1815,10 +1816,10 @@ export const listarAgendamentosChat = query({
return mensagensEnriquecidas
.filter((m): m is NonNullable
=> m !== null)
.sort((a, b) => {
- const dataA = a.agendadaPara ?? 0;
- const dataB = b.agendadaPara ?? 0;
- return dataA - dataB;
- });
+ const dataA = a.agendadaPara ?? 0;
+ const dataB = b.agendadaPara ?? 0;
+ return dataA - dataB;
+ });
},
});
@@ -2071,7 +2072,7 @@ export const buscarMensagens = query({
// Filtrar por remetente (já verificado acima, mas garantir novamente)
if (args.remetenteId) {
if (m.remetenteId !== args.remetenteId) {
- return false;
+ return false;
}
// Verificar novamente se o remetente é participante da conversa específica desta mensagem
if (!conversaDaMensagem.participantes.includes(args.remetenteId)) {
@@ -2269,6 +2270,7 @@ export const enviarMensagensAgendadas = internalMutation({
await ctx.db.patch(mensagem.conversaId, {
ultimaMensagem: mensagem.conteudo.substring(0, 100),
ultimaMensagemTimestamp: agora,
+ ultimaMensagemRemetenteId: mensagem.remetenteId, // Guardar ID do remetente
});
// Criar notificações para outros participantes
diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts
index 28bfdae..7a3fb75 100644
--- a/packages/backend/convex/schema.ts
+++ b/packages/backend/convex/schema.ts
@@ -626,6 +626,7 @@ export default defineSchema({
administradores: v.optional(v.array(v.id("usuarios"))), // IDs dos administradores (apenas para sala_reuniao)
ultimaMensagem: v.optional(v.string()),
ultimaMensagemTimestamp: v.optional(v.number()),
+ ultimaMensagemRemetenteId: v.optional(v.id("usuarios")), // ID do remetente da última mensagem
criadoPor: v.id("usuarios"),
criadoEm: v.number(),
})