- Updated type definitions in ChatWindow and MessageList components for better type safety. - Improved MessageInput to handle message responses, including a preview feature for replying to messages. - Enhanced the chat message handling logic to support message references and improve user interaction. - Refactored notification utility functions to support push notifications and rate limiting for email sending. - Updated backend schema to accommodate new features related to message responses and notifications.
267 lines
6.9 KiB
TypeScript
267 lines
6.9 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query, internalMutation, internalQuery } from "./_generated/server";
|
|
import { Id } from "./_generated/dataModel";
|
|
import { internal, api } from "./_generated/api";
|
|
|
|
/**
|
|
* Registrar subscription de push notification
|
|
*/
|
|
export const registrarPushSubscription = mutation({
|
|
args: {
|
|
endpoint: v.string(),
|
|
keys: v.object({
|
|
p256dh: v.string(),
|
|
auth: v.string(),
|
|
}),
|
|
userAgent: v.optional(v.string()),
|
|
},
|
|
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
|
handler: async (ctx, args) => {
|
|
// Obter usuário autenticado
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity?.email) {
|
|
return { sucesso: false, erro: "Usuário não autenticado" };
|
|
}
|
|
|
|
const usuario = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
|
.first();
|
|
|
|
if (!usuario) {
|
|
return { sucesso: false, erro: "Usuário não encontrado" };
|
|
}
|
|
|
|
// Verificar se já existe subscription com este endpoint
|
|
const existente = await ctx.db
|
|
.query("pushSubscriptions")
|
|
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
|
|
.first();
|
|
|
|
if (existente) {
|
|
// Atualizar subscription existente
|
|
await ctx.db.patch(existente._id, {
|
|
usuarioId: usuario._id,
|
|
keys: args.keys,
|
|
userAgent: args.userAgent,
|
|
ultimaAtividade: Date.now(),
|
|
ativo: true,
|
|
});
|
|
} else {
|
|
// Criar nova subscription
|
|
await ctx.db.insert("pushSubscriptions", {
|
|
usuarioId: usuario._id,
|
|
endpoint: args.endpoint,
|
|
keys: args.keys,
|
|
userAgent: args.userAgent,
|
|
criadoEm: Date.now(),
|
|
ultimaAtividade: Date.now(),
|
|
ativo: true,
|
|
});
|
|
}
|
|
|
|
return { sucesso: true };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Remover subscription de push notification
|
|
*/
|
|
export const removerPushSubscription = mutation({
|
|
args: {
|
|
endpoint: v.string(),
|
|
},
|
|
returns: v.object({ sucesso: v.boolean() }),
|
|
handler: async (ctx, args) => {
|
|
const subscription = await ctx.db
|
|
.query("pushSubscriptions")
|
|
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
|
|
.first();
|
|
|
|
if (subscription) {
|
|
await ctx.db.patch(subscription._id, { ativo: false });
|
|
}
|
|
|
|
return { sucesso: true };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Obter subscriptions ativas de um usuário
|
|
*/
|
|
export const obterPushSubscriptions = internalQuery({
|
|
args: {
|
|
usuarioId: v.id("usuarios"),
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id("pushSubscriptions"),
|
|
endpoint: v.string(),
|
|
keys: v.object({
|
|
p256dh: v.string(),
|
|
auth: v.string(),
|
|
}),
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const subscriptions = await ctx.db
|
|
.query("pushSubscriptions")
|
|
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId).eq("ativo", true))
|
|
.collect();
|
|
|
|
return subscriptions.map((sub) => ({
|
|
_id: sub._id,
|
|
endpoint: sub.endpoint,
|
|
keys: sub.keys,
|
|
}));
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Enviar push notification para um usuário
|
|
* Esta função será chamada quando uma nova mensagem chegar
|
|
*/
|
|
export const enviarPushNotification = internalMutation({
|
|
args: {
|
|
usuarioId: v.id("usuarios"),
|
|
titulo: v.string(),
|
|
corpo: v.string(),
|
|
data: v.optional(
|
|
v.object({
|
|
conversaId: v.optional(v.id("conversas")),
|
|
mensagemId: v.optional(v.id("mensagens")),
|
|
tipo: v.optional(v.string()),
|
|
})
|
|
),
|
|
},
|
|
returns: v.object({ enviados: v.number(), falhas: v.number() }),
|
|
handler: async (ctx, args) => {
|
|
// Buscar subscriptions ativas do usuário
|
|
const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, {
|
|
usuarioId: args.usuarioId,
|
|
});
|
|
|
|
if (subscriptions.length === 0) {
|
|
return { enviados: 0, falhas: 0 };
|
|
}
|
|
|
|
// Verificar preferências do usuário
|
|
const usuario = await ctx.db.get(args.usuarioId);
|
|
if (!usuario || usuario.notificacoesAtivadas === false) {
|
|
return { enviados: 0, falhas: 0 };
|
|
}
|
|
|
|
// Se há conversaId, verificar preferências específicas da conversa
|
|
if (args.data?.conversaId) {
|
|
const preferencias = await ctx.db
|
|
.query("preferenciasNotificacaoConversa")
|
|
.withIndex("by_usuario_conversa", (q) =>
|
|
q.eq("usuarioId", args.usuarioId).eq("conversaId", args.data.conversaId)
|
|
)
|
|
.first();
|
|
|
|
if (preferencias) {
|
|
// Se silenciado ou push desativado, não enviar
|
|
if (preferencias.silenciado || !preferencias.pushAtivado) {
|
|
return { enviados: 0, falhas: 0 };
|
|
}
|
|
|
|
// Se apenas menções e não é menção, não enviar
|
|
if (preferencias.apenasMencoes && args.data.tipo !== "mencao") {
|
|
return { enviados: 0, falhas: 0 };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Agendar envio de push via action (que roda em Node.js)
|
|
let enviados = 0;
|
|
let falhas = 0;
|
|
|
|
for (const subscription of subscriptions) {
|
|
try {
|
|
await ctx.scheduler.runAfter(0, api.actions.pushNotifications.enviarPush, {
|
|
subscriptionId: subscription._id,
|
|
titulo: args.titulo,
|
|
corpo: args.corpo,
|
|
data: args.data,
|
|
});
|
|
enviados++;
|
|
} catch (error) {
|
|
console.error(`Erro ao agendar push para subscription ${subscription._id}:`, error);
|
|
falhas++;
|
|
}
|
|
}
|
|
|
|
return { enviados, falhas };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Obter subscription por ID (para actions)
|
|
*/
|
|
export const getSubscriptionById = internalQuery({
|
|
args: {
|
|
subscriptionId: v.id("pushSubscriptions"),
|
|
},
|
|
returns: v.union(
|
|
v.object({
|
|
_id: v.id("pushSubscriptions"),
|
|
endpoint: v.string(),
|
|
keys: v.object({
|
|
p256dh: v.string(),
|
|
auth: v.string(),
|
|
}),
|
|
ativo: v.boolean(),
|
|
}),
|
|
v.null()
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const subscription = await ctx.db.get(args.subscriptionId);
|
|
if (!subscription) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
_id: subscription._id,
|
|
endpoint: subscription.endpoint,
|
|
keys: subscription.keys,
|
|
ativo: subscription.ativo,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Marcar subscription como inativa
|
|
*/
|
|
export const marcarSubscriptionInativa = internalMutation({
|
|
args: {
|
|
subscriptionId: v.id("pushSubscriptions"),
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
await ctx.db.patch(args.subscriptionId, { ativo: false });
|
|
return null;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Verificar se usuário está online (última atividade recente)
|
|
*/
|
|
export const verificarUsuarioOnline = internalQuery({
|
|
args: {
|
|
usuarioId: v.id("usuarios"),
|
|
},
|
|
returns: v.boolean(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await ctx.db.get(args.usuarioId);
|
|
if (!usuario || !usuario.ultimaAtividade) {
|
|
return false;
|
|
}
|
|
|
|
// Considerar online se última atividade foi há menos de 5 minutos
|
|
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
|
|
return usuario.ultimaAtividade >= cincoMinutosAtras;
|
|
},
|
|
});
|
|
|