feat: enhance chat functionality with global notifications and user management
- Implemented global notifications for new messages, allowing users to receive alerts even when the chat is minimized or closed. - Added functionality for users to leave group conversations and meeting rooms, with appropriate notifications sent to remaining participants. - Introduced a modal for sending notifications within meeting rooms, enabling admins to communicate important messages to all participants. - Enhanced the chat components to support mention functionality, allowing users to tag participants in messages for better engagement. - Updated backend mutations to handle user exit from conversations and sending notifications, ensuring robust data handling and user experience.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { useQuery } from "convex-svelte";
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { voltarParaLista } from "$lib/stores/chatStore";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
@@ -10,6 +10,7 @@
|
||||
import ScheduleMessageModal from "./ScheduleMessageModal.svelte";
|
||||
import SalaReuniaoManager from "./SalaReuniaoManager.svelte";
|
||||
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
|
||||
interface Props {
|
||||
conversaId: string;
|
||||
@@ -17,8 +18,12 @@
|
||||
|
||||
let { conversaId }: Props = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
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 any });
|
||||
@@ -73,11 +78,36 @@
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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"}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultado = await client.mutation(api.chat.sairGrupoOuSala, {
|
||||
conversaId: conversaId as any,
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
voltarParaLista();
|
||||
} else {
|
||||
alert(resultado.erro || "Erro ao sair da conversa");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao sair da conversa:", error);
|
||||
alert(error.message || "Erro ao sair da conversa");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex flex-col h-full" onclick={() => (showAdminMenu = false)}>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-3 px-4 py-3 border-b border-base-300 bg-base-200">
|
||||
<div class="flex items-center gap-3 px-4 py-3 border-b border-base-300 bg-base-200" onclick={(e) => e.stopPropagation()}>
|
||||
<!-- Botão Voltar -->
|
||||
<button
|
||||
type="button"
|
||||
@@ -170,17 +200,20 @@
|
||||
|
||||
<!-- Botões de ação -->
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Botão Gerenciar Sala (apenas para salas de reunião) -->
|
||||
{#if conversa()?.tipo === "sala_reuniao"}
|
||||
<!-- Botão Sair (apenas para grupos e salas de reunião) -->
|
||||
{#if conversa() && (conversa()?.tipo === "grupo" || conversa()?.tipo === "sala_reuniao")}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
||||
style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2);"
|
||||
onclick={() => (showSalaManager = true)}
|
||||
aria-label="Gerenciar sala"
|
||||
title="Gerenciar sala de reunião"
|
||||
style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2);"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSairGrupoOuSala();
|
||||
}}
|
||||
aria-label="Sair"
|
||||
title="Sair da conversa"
|
||||
>
|
||||
<div class="absolute inset-0 bg-blue-500/0 group-hover:bg-blue-500/10 transition-colors duration-300"></div>
|
||||
<div class="absolute inset-0 bg-red-500/0 group-hover:bg-red-500/10 transition-colors duration-300"></div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -189,13 +222,117 @@
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5 text-blue-500 relative z-10 group-hover:scale-110 transition-transform"
|
||||
class="w-5 h-5 text-red-500 relative z-10 group-hover:scale-110 transition-transform"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24"/>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Botão Menu Administrativo (apenas para salas de reunião e apenas para admins) -->
|
||||
{#if conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
|
||||
<div class="relative admin-menu-container">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
||||
style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2);"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
showAdminMenu = !showAdminMenu;
|
||||
}}
|
||||
aria-label="Menu administrativo"
|
||||
title="Recursos administrativos"
|
||||
>
|
||||
<div class="absolute inset-0 bg-blue-500/0 group-hover:bg-blue-500/10 transition-colors duration-300"></div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5 text-blue-500 relative z-10 group-hover:scale-110 transition-transform"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="12" cy="5" r="1"/>
|
||||
<circle cx="12" cy="19" r="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
{#if showAdminMenu}
|
||||
<ul
|
||||
class="absolute right-0 top-full mt-2 bg-base-100 rounded-lg shadow-xl border border-base-300 w-56 z-[100] overflow-hidden"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 transition-colors flex items-center gap-2"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
showSalaManager = true;
|
||||
showAdminMenu = false;
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
|
||||
</svg>
|
||||
Gerenciar Participantes
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 transition-colors flex items-center gap-2"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
showNotificacaoModal = true;
|
||||
showAdminMenu = false;
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
||||
</svg>
|
||||
Enviar Notificação
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-error/10 transition-colors flex items-center gap-2 text-error"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
(async () => {
|
||||
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 any,
|
||||
});
|
||||
if (resultado.sucesso) {
|
||||
alert("Reunião encerrada com sucesso!");
|
||||
voltarParaLista();
|
||||
} else {
|
||||
alert(resultado.erro || "Erro ao encerrar reunião");
|
||||
}
|
||||
} catch (error: any) {
|
||||
alert(error.message || "Erro ao encerrar reunião");
|
||||
}
|
||||
showAdminMenu = false;
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Encerrar Reunião
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Botão Agendar MODERNO -->
|
||||
<button
|
||||
@@ -252,3 +389,91 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Modal de Enviar Notificação -->
|
||||
{#if showNotificacaoModal && conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50" onclick={() => (showNotificacaoModal = false)}>
|
||||
<div
|
||||
class="bg-base-100 rounded-xl shadow-2xl w-full max-w-md m-4"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300">
|
||||
<h2 class="text-xl font-semibold">Enviar Notificação</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-circle"
|
||||
onclick={() => (showNotificacaoModal = false)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const titulo = formData.get("titulo") as string;
|
||||
const mensagem = formData.get("mensagem") as string;
|
||||
|
||||
if (!titulo.trim() || !mensagem.trim()) {
|
||||
alert("Preencha todos os campos");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultado = await client.mutation(api.chat.enviarNotificacaoReuniao, {
|
||||
conversaId: conversaId as any,
|
||||
titulo: titulo.trim(),
|
||||
mensagem: mensagem.trim(),
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
alert("Notificação enviada com sucesso!");
|
||||
showNotificacaoModal = false;
|
||||
} else {
|
||||
alert(resultado.erro || "Erro ao enviar notificação");
|
||||
}
|
||||
} catch (error: any) {
|
||||
alert(error.message || "Erro ao enviar notificação");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Título</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="titulo"
|
||||
placeholder="Título da notificação"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Mensagem</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="mensagem"
|
||||
placeholder="Mensagem da notificação"
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="4"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn btn-ghost flex-1" onclick={() => (showNotificacaoModal = false)}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary flex-1">
|
||||
Enviar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user