import { v } from "convex/values"; import { mutation, query, internalMutation } from "./_generated/server"; import { Doc, Id } from "./_generated/dataModel"; import type { QueryCtx, MutationCtx } from "./_generated/server"; // ========== HELPERS ========== /** * Helper function para obter usuário autenticado (Better Auth ou Sessão) */ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) { // Tentar autenticação via Better Auth primeiro const identity = await ctx.auth.getUserIdentity(); let usuarioAtual = null; if (identity && identity.email) { usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } // Se não encontrou via Better Auth, tentar via sessão if (!usuarioAtual) { const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .first(); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } } return usuarioAtual; } // ========== MUTATIONS ========== /** * Cria uma nova conversa (individual ou grupo) */ export const criarConversa = mutation({ args: { tipo: v.union(v.literal("individual"), v.literal("grupo")), participantes: v.array(v.id("usuarios")), nome: v.optional(v.string()), avatar: v.optional(v.string()), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); // Validar participantes if (!args.participantes.includes(usuarioAtual._id)) { args.participantes.push(usuarioAtual._id); } // Se for conversa individual, verificar se já existe if (args.tipo === "individual" && args.participantes.length === 2) { const conversaExistente = await ctx.db .query("conversas") .filter((q) => q.eq(q.field("tipo"), "individual")) .collect(); for (const conversa of conversaExistente) { if ( conversa.participantes.length === 2 && conversa.participantes.every((p) => args.participantes.includes(p)) ) { return conversa._id; } } } // Criar nova conversa const conversaId = await ctx.db.insert("conversas", { tipo: args.tipo, nome: args.nome, avatar: args.avatar, participantes: args.participantes, criadoPor: usuarioAtual._id, criadoEm: Date.now(), }); // Criar notificações para outros participantes if (args.tipo === "grupo") { for (const participanteId of args.participantes) { if (participanteId !== usuarioAtual._id) { await ctx.db.insert("notificacoes", { usuarioId: participanteId, tipo: "adicionado_grupo", conversaId, remetenteId: usuarioAtual._id, titulo: "Adicionado a grupo", descricao: `Você foi adicionado ao grupo "${args.nome || "Sem nome"}" por ${usuarioAtual.nome}`, lida: false, criadaEm: Date.now(), }); } } } return conversaId; }, }); /** * Cria ou busca uma conversa individual com outro usuário */ export const criarOuBuscarConversaIndividual = mutation({ args: { outroUsuarioId: v.id("usuarios"), }, returns: v.id("conversas"), handler: async (ctx, args) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); let usuarioAtual = null; if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } } if (!usuarioAtual) throw new Error("Usuário não autenticado"); // Buscar conversa individual existente entre os dois usuários const conversasExistentes = await ctx.db .query("conversas") .filter((q) => q.eq(q.field("tipo"), "individual")) .collect(); for (const conversa of conversasExistentes) { if ( conversa.participantes.length === 2 && conversa.participantes.includes(usuarioAtual._id) && conversa.participantes.includes(args.outroUsuarioId) ) { return conversa._id; } } // Se não existe, criar nova conversa individual const conversaId = await ctx.db.insert("conversas", { tipo: "individual", participantes: [usuarioAtual._id, args.outroUsuarioId], criadoPor: usuarioAtual._id, criadoEm: Date.now(), }); return conversaId; }, }); /** * Envia uma mensagem em uma conversa */ export const enviarMensagem = mutation({ args: { conversaId: v.id("conversas"), conteudo: v.string(), tipo: v.union( v.literal("texto"), v.literal("arquivo"), v.literal("imagem") ), arquivoId: v.optional(v.id("_storage")), arquivoNome: v.optional(v.string()), arquivoTamanho: v.optional(v.number()), arquivoTipo: v.optional(v.string()), mencoes: v.optional(v.array(v.id("usuarios"))), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); // Verificar se usuário pertence à conversa const conversa = await ctx.db.get(args.conversaId); if (!conversa) throw new Error("Conversa não encontrada"); if (!conversa.participantes.includes(usuarioAtual._id)) { throw new Error("Você não pertence a esta conversa"); } // Criar mensagem const mensagemId = await ctx.db.insert("mensagens", { conversaId: args.conversaId, remetenteId: usuarioAtual._id, tipo: args.tipo, conteudo: args.conteudo, arquivoId: args.arquivoId, arquivoNome: args.arquivoNome, arquivoTamanho: args.arquivoTamanho, arquivoTipo: args.arquivoTipo, mencoes: args.mencoes, enviadaEm: Date.now(), }); // Atualizar última mensagem da conversa await ctx.db.patch(args.conversaId, { ultimaMensagem: args.conteudo.substring(0, 100), ultimaMensagemTimestamp: Date.now(), }); // Criar notificações para outros participantes (com tratamento de erro) try { for (const participanteId of conversa.participantes) { if (participanteId !== usuarioAtual._id) { const tipoNotificacao = args.mencoes?.includes(participanteId) ? "mencao" : "nova_mensagem"; await ctx.db.insert("notificacoes", { usuarioId: participanteId, tipo: tipoNotificacao, conversaId: args.conversaId, mensagemId, remetenteId: usuarioAtual._id, titulo: tipoNotificacao === "mencao" ? `${usuarioAtual.nome} mencionou você` : `Nova mensagem de ${usuarioAtual.nome}`, descricao: args.conteudo.substring(0, 100), lida: false, criadaEm: Date.now(), }); } } } catch (error) { // Log do erro mas não falhar o envio da mensagem console.error("Erro ao criar notificações:", error); // A mensagem já foi criada, então retornamos o ID normalmente } return mensagemId; }, }); /** * Agenda uma mensagem para envio futuro */ export const agendarMensagem = mutation({ args: { conversaId: v.id("conversas"), conteudo: v.string(), agendadaPara: v.number(), // timestamp }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); // Validar data futura if (args.agendadaPara <= Date.now()) { throw new Error("Data de agendamento deve ser futura"); } // Verificar se usuário pertence à conversa const conversa = await ctx.db.get(args.conversaId); if (!conversa) throw new Error("Conversa não encontrada"); if (!conversa.participantes.includes(usuarioAtual._id)) { throw new Error("Você não pertence a esta conversa"); } // Criar mensagem agendada const mensagemId = await ctx.db.insert("mensagens", { conversaId: args.conversaId, remetenteId: usuarioAtual._id, tipo: "texto", conteudo: args.conteudo, agendadaPara: args.agendadaPara, enviadaEm: args.agendadaPara, // Será usada quando a mensagem for enviada }); return mensagemId; }, }); /** * Cancela uma mensagem agendada */ export const cancelarMensagemAgendada = mutation({ args: { mensagemId: v.id("mensagens"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); const mensagem = await ctx.db.get(args.mensagemId); if (!mensagem) throw new Error("Mensagem não encontrada"); if (mensagem.remetenteId !== usuarioAtual._id) { throw new Error("Você só pode cancelar suas próprias mensagens"); } await ctx.db.delete(args.mensagemId); return true; }, }); /** * Adiciona uma reação (emoji) a uma mensagem */ export const reagirMensagem = mutation({ args: { mensagemId: v.id("mensagens"), emoji: v.string(), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); const mensagem = await ctx.db.get(args.mensagemId); if (!mensagem) throw new Error("Mensagem não encontrada"); const reacoes = mensagem.reagiuPor || []; const reacaoExistente = reacoes.find( (r) => r.usuarioId === usuarioAtual._id && r.emoji === args.emoji ); if (reacaoExistente) { // Remover reação await ctx.db.patch(args.mensagemId, { reagiuPor: reacoes.filter( (r) => !(r.usuarioId === usuarioAtual._id && r.emoji === args.emoji) ), }); } else { // Adicionar reação await ctx.db.patch(args.mensagemId, { reagiuPor: [ ...reacoes, { usuarioId: usuarioAtual._id, emoji: args.emoji }, ], }); } return true; }, }); /** * Marca mensagens de uma conversa como lidas */ export const marcarComoLida = mutation({ args: { conversaId: v.id("conversas"), mensagemId: v.id("mensagens"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); // Buscar registro de leitura existente const leituraExistente = await ctx.db .query("leituras") .withIndex("by_conversa_usuario", (q) => q.eq("conversaId", args.conversaId).eq("usuarioId", usuarioAtual._id) ) .first(); if (leituraExistente) { await ctx.db.patch(leituraExistente._id, { ultimaMensagemLida: args.mensagemId, lidaEm: Date.now(), }); } else { await ctx.db.insert("leituras", { conversaId: args.conversaId, usuarioId: usuarioAtual._id, ultimaMensagemLida: args.mensagemId, lidaEm: Date.now(), }); } // Marcar notificações desta conversa como lidas const notificacoes = await ctx.db .query("notificacoes") .withIndex("by_usuario_lida", (q) => q.eq("usuarioId", usuarioAtual._id).eq("lida", false) ) .filter((q) => q.eq(q.field("conversaId"), args.conversaId)) .collect(); for (const notificacao of notificacoes) { await ctx.db.patch(notificacao._id, { lida: true }); } return true; }, }); /** * Atualiza o status de presença do usuário */ export const atualizarStatusPresenca = mutation({ args: { status: v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); await ctx.db.patch(usuarioAtual._id, { statusPresenca: args.status, ultimaAtividade: Date.now(), }); return true; }, }); /** * Indica que o usuário está digitando em uma conversa */ export const indicarDigitacao = mutation({ args: { conversaId: v.id("conversas"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); // Buscar indicador existente const indicadorExistente = await ctx.db .query("digitando") .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioAtual._id)) .filter((q) => q.eq(q.field("conversaId"), args.conversaId)) .first(); if (indicadorExistente) { await ctx.db.patch(indicadorExistente._id, { iniciouEm: Date.now(), }); } else { await ctx.db.insert("digitando", { conversaId: args.conversaId, usuarioId: usuarioAtual._id, iniciouEm: Date.now(), }); } return true; }, }); /** * Gera URL para upload de arquivo no chat */ export const uploadArquivoChat = mutation({ args: { conversaId: v.id("conversas"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); // Verificar se usuário pertence à conversa const conversa = await ctx.db.get(args.conversaId); if (!conversa) throw new Error("Conversa não encontrada"); if (!conversa.participantes.includes(usuarioAtual._id)) { throw new Error("Você não pertence a esta conversa"); } return await ctx.storage.generateUploadUrl(); }, }); /** * Marca uma notificação como lida */ export const marcarNotificacaoLida = mutation({ args: { notificacaoId: v.id("notificacoes"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); await ctx.db.patch(args.notificacaoId, { lida: true }); return true; }, }); /** * Marca todas as notificações como lidas */ export const marcarTodasNotificacoesLidas = mutation({ args: {}, handler: async (ctx) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); const notificacoes = await ctx.db .query("notificacoes") .withIndex("by_usuario_lida", (q) => q.eq("usuarioId", usuarioAtual._id).eq("lida", false) ) .collect(); for (const notificacao of notificacoes) { await ctx.db.patch(notificacao._id, { lida: true }); } return true; }, }); /** * Deleta uma mensagem (soft delete) */ export const deletarMensagem = mutation({ args: { mensagemId: v.id("mensagens"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) throw new Error("Não autenticado"); const mensagem = await ctx.db.get(args.mensagemId); if (!mensagem) throw new Error("Mensagem não encontrada"); if (mensagem.remetenteId !== usuarioAtual._id) { throw new Error("Você só pode deletar suas próprias mensagens"); } await ctx.db.patch(args.mensagemId, { deletada: true, conteudo: "Mensagem deletada", }); return true; }, }); // ========== QUERIES ========== /** * Lista todas as conversas do usuário logado */ export const listarConversas = query({ args: {}, handler: async (ctx) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; // Buscar todas as conversas do usuário const todasConversas = await ctx.db.query("conversas").collect(); const conversasDoUsuario = todasConversas.filter((c) => c.participantes.includes(usuarioAtual._id) ); // Ordenar por última mensagem conversasDoUsuario.sort((a, b) => { const timestampA = a.ultimaMensagemTimestamp || a.criadoEm; const timestampB = b.ultimaMensagemTimestamp || b.criadoEm; return timestampB - timestampA; }); // Enriquecer com informações dos participantes const conversasEnriquecidas = await Promise.all( conversasDoUsuario.map(async (conversa) => { // Buscar participantes const participantes = await Promise.all( conversa.participantes.map((id) => ctx.db.get(id)) ); // Para conversas individuais, pegar o outro usuário let outroUsuario = null; if (conversa.tipo === "individual") { const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id); if (outroUsuarioRaw) { // Adicionar URL da foto de perfil let fotoPerfilUrl = null; if (outroUsuarioRaw.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(outroUsuarioRaw.fotoPerfil); } outroUsuario = { ...outroUsuarioRaw, fotoPerfilUrl, }; } } // Contar mensagens não lidas (apenas mensagens NÃO agendadas) const leitura = await ctx.db .query("leituras") .withIndex("by_conversa_usuario", (q) => q.eq("conversaId", conversa._id).eq("usuarioId", usuarioAtual._id) ) .first(); // CORRIGIDO: Buscar apenas mensagens NÃO agendadas (agendadaPara === undefined) const todasMensagens = await ctx.db .query("mensagens") .withIndex("by_conversa", (q) => q.eq("conversaId", conversa._id)) .collect(); const mensagens = todasMensagens.filter((m) => !m.agendadaPara); let naoLidas = 0; if (leitura) { naoLidas = mensagens.filter( (m) => m.enviadaEm > (leitura.lidaEm || 0) && m.remetenteId !== usuarioAtual._id ).length; } else { naoLidas = mensagens.filter( (m) => m.remetenteId !== usuarioAtual._id ).length; } return { ...conversa, outroUsuario, participantesInfo: participantes.filter((p) => p !== null), naoLidas, }; }) ); return conversasEnriquecidas; }, }); /** * Obtém as mensagens de uma conversa com paginação */ export const obterMensagens = query({ args: { conversaId: v.id("conversas"), limit: v.optional(v.number()), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; // Verificar se usuário pertence à conversa const conversa = await ctx.db.get(args.conversaId); if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { return []; } // Buscar mensagens (excluir agendadas) const mensagens = await ctx.db .query("mensagens") .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) .order("desc") .take(args.limit || 50); // Filtrar mensagens agendadas const mensagensFiltradas = mensagens.filter((m) => !m.agendadaPara); // Enriquecer com informações do remetente const mensagensEnriquecidas = await Promise.all( mensagensFiltradas.map(async (mensagem) => { const remetente = await ctx.db.get(mensagem.remetenteId); let arquivoUrl = null; if (mensagem.arquivoId) { arquivoUrl = await ctx.storage.getUrl(mensagem.arquivoId); } return { ...mensagem, remetente, arquivoUrl, }; }) ); return mensagensEnriquecidas.reverse(); }, }); /** * Obtém mensagens agendadas de uma conversa */ export const obterMensagensAgendadas = query({ args: { conversaId: v.id("conversas"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; // Buscar mensagens agendadas const todasMensagens = await ctx.db .query("mensagens") .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) .collect(); // Filtrar apenas as agendadas do usuário atual const minhasMensagensAgendadas = todasMensagens.filter( (m) => m.remetenteId === usuarioAtual._id && m.agendadaPara && m.agendadaPara > Date.now() ); return minhasMensagensAgendadas.sort( (a, b) => (a.agendadaPara || 0) - (b.agendadaPara || 0) ); }, }); /** * Obtém as notificações do usuário */ export const obterNotificacoes = query({ args: { apenasPendentes: v.optional(v.boolean()), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; let query = ctx.db .query("notificacoes") .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioAtual._id)); if (args.apenasPendentes) { query = ctx.db .query("notificacoes") .withIndex("by_usuario_lida", (q) => q.eq("usuarioId", usuarioAtual._id).eq("lida", false) ); } const notificacoes = await query.order("desc").take(50); // Enriquecer com informações do remetente const notificacoesEnriquecidas = await Promise.all( notificacoes.map(async (notificacao) => { let remetente = null; if (notificacao.remetenteId) { remetente = await ctx.db.get(notificacao.remetenteId); } return { ...notificacao, remetente, }; }) ); return notificacoesEnriquecidas; }, }); /** * Conta o número de notificações não lidas */ export const contarNotificacoesNaoLidas = query({ args: {}, handler: async (ctx) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return 0; const notificacoes = await ctx.db .query("notificacoes") .withIndex("by_usuario_lida", (q) => q.eq("usuarioId", usuarioAtual._id).eq("lida", false) ) .collect(); return notificacoes.length; }, }); /** * Obtém usuários online */ export const obterUsuariosOnline = query({ args: {}, handler: async (ctx) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; const usuarios = await ctx.db .query("usuarios") .withIndex("by_status_presenca", (q) => q.eq("statusPresenca", "online")) .collect(); return usuarios.map((u) => ({ _id: u._id, nome: u.nome, email: u.email, avatar: u.avatar, fotoPerfil: u.fotoPerfil, statusPresenca: u.statusPresenca, statusMensagem: u.statusMensagem, setor: u.setor, })); }, }); /** * Lista todos os usuários (para criar nova conversa) */ export const listarTodosUsuarios = query({ args: {}, handler: async (ctx) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; const usuarios = await ctx.db .query("usuarios") .withIndex("by_ativo", (q) => q.eq("ativo", true)) .collect(); // Excluir o usuário atual return usuarios .filter((u) => u._id !== usuarioAtual._id) .map((u) => ({ _id: u._id, nome: u.nome, email: u.email, matricula: u.matricula, avatar: u.avatar, fotoPerfil: u.fotoPerfil, statusPresenca: u.statusPresenca, statusMensagem: u.statusMensagem, setor: u.setor, })); }, }); /** * Busca mensagens em conversas */ export const buscarMensagens = query({ args: { query: v.string(), conversaId: v.optional(v.id("conversas")), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; // Buscar em todas as conversas do usuário const todasConversas = await ctx.db.query("conversas").collect(); const conversasDoUsuario = todasConversas.filter((c) => c.participantes.includes(usuarioAtual._id) ); let mensagens: any[] = []; if (args.conversaId !== undefined) { // Buscar em conversa específica const mensagensConversa = await ctx.db .query("mensagens") .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId!)) .collect(); mensagens = mensagensConversa; } else { // Buscar em todas as conversas for (const conversa of conversasDoUsuario) { const mensagensConversa = await ctx.db .query("mensagens") .withIndex("by_conversa", (q) => q.eq("conversaId", conversa._id)) .collect(); mensagens.push(...mensagensConversa); } } // Filtrar por query const queryLower = args.query.toLowerCase(); const mensagensFiltradas = mensagens.filter( (m) => !m.deletada && !m.agendadaPara && m.conteudo.toLowerCase().includes(queryLower) ); // Enriquecer com informações const mensagensEnriquecidas = await Promise.all( mensagensFiltradas.map(async (mensagem) => { const remetente = await ctx.db.get(mensagem.remetenteId); const conversa = await ctx.db.get(mensagem.conversaId); return { ...mensagem, remetente, conversa, }; }) ); return mensagensEnriquecidas .sort((a, b) => b.enviadaEm - a.enviadaEm) .slice(0, 50); }, }); /** * Obtém quem está digitando em uma conversa */ export const obterDigitando = query({ args: { conversaId: v.id("conversas"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return []; // Buscar indicadores de digitação (últimos 10 segundos) const dezSegundosAtras = Date.now() - 10000; const digitando = await ctx.db .query("digitando") .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) .filter((q) => q.gte(q.field("iniciouEm"), dezSegundosAtras)) .collect(); // Filtrar usuário atual e buscar informações const digitandoFiltrado = digitando.filter( (d) => d.usuarioId !== usuarioAtual._id ); const usuarios = await Promise.all( digitandoFiltrado.map(async (d) => { const usuario = await ctx.db.get(d.usuarioId); return usuario; }) ); return usuarios.filter((u) => u !== null); }, }); /** * Conta mensagens não lidas de uma conversa */ export const contarNaoLidas = query({ args: { conversaId: v.id("conversas"), }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); if (!usuarioAtual) return 0; const leitura = await ctx.db .query("leituras") .withIndex("by_conversa_usuario", (q) => q.eq("conversaId", args.conversaId).eq("usuarioId", usuarioAtual._id) ) .first(); const mensagens = await ctx.db .query("mensagens") .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) .filter((q) => q.eq(q.field("agendadaPara"), undefined)) .collect(); if (leitura) { return mensagens.filter( (m) => m.enviadaEm > (leitura.lidaEm || 0) && m.remetenteId !== usuarioAtual._id ).length; } return mensagens.filter((m) => m.remetenteId !== usuarioAtual._id).length; }, }); // ========== INTERNAL MUTATIONS (para crons) ========== /** * Envia mensagens agendadas (chamado pelo cron) */ export const enviarMensagensAgendadas = internalMutation({ args: {}, handler: async (ctx) => { const agora = Date.now(); // Buscar mensagens que deveriam ser enviadas const mensagensAgendadas = await ctx.db .query("mensagens") .withIndex("by_agendamento") .filter((q) => q.and( q.neq(q.field("agendadaPara"), undefined), q.lte(q.field("agendadaPara"), agora) ) ) .collect(); for (const mensagem of mensagensAgendadas) { // Atualizar mensagem para "enviada" await ctx.db.patch(mensagem._id, { agendadaPara: undefined, enviadaEm: agora, }); // Atualizar última mensagem da conversa const conversa = await ctx.db.get(mensagem.conversaId); if (conversa) { await ctx.db.patch(mensagem.conversaId, { ultimaMensagem: mensagem.conteudo.substring(0, 100), ultimaMensagemTimestamp: agora, }); // Criar notificações para outros participantes const remetente = await ctx.db.get(mensagem.remetenteId); for (const participanteId of conversa.participantes) { if (participanteId !== mensagem.remetenteId) { await ctx.db.insert("notificacoes", { usuarioId: participanteId, tipo: "nova_mensagem", conversaId: mensagem.conversaId, mensagemId: mensagem._id, remetenteId: mensagem.remetenteId, titulo: `Nova mensagem de ${remetente?.nome || "Usuário"}`, descricao: mensagem.conteudo.substring(0, 100), lida: false, criadaEm: agora, }); } } } } return mensagensAgendadas.length; }, }); /** * Limpa indicadores de digitação antigos (chamado pelo cron) */ export const limparIndicadoresDigitacao = internalMutation({ args: {}, handler: async (ctx) => { const dezSegundosAtras = Date.now() - 10000; const indicadoresAntigos = await ctx.db .query("digitando") .filter((q) => q.lt(q.field("iniciouEm"), dezSegundosAtras)) .collect(); for (const indicador of indicadoresAntigos) { await ctx.db.delete(indicador._id); } return indicadoresAntigos.length; }, });