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,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from "convex-svelte";
|
||||
import { useConvexClient, useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { onMount } from "svelte";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
|
||||
interface Props {
|
||||
conversaId: Id<"conversas">;
|
||||
@@ -11,6 +12,7 @@
|
||||
let { conversaId }: Props = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
const conversas = useQuery(api.chat.listarConversas, {});
|
||||
|
||||
let mensagem = $state("");
|
||||
let textarea: HTMLTextAreaElement;
|
||||
@@ -19,6 +21,9 @@
|
||||
let digitacaoTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let showEmojiPicker = $state(false);
|
||||
let mensagemRespondendo: { id: Id<"mensagens">; conteudo: string; remetente: string } | null = $state(null);
|
||||
let showMentionsDropdown = $state(false);
|
||||
let mentionQuery = $state("");
|
||||
let mentionStartPos = $state(0);
|
||||
|
||||
// Emojis mais usados
|
||||
const emojis = [
|
||||
@@ -37,13 +42,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-resize do textarea
|
||||
function handleInput() {
|
||||
// Obter conversa atual
|
||||
const conversa = $derived(() => {
|
||||
if (!conversas?.data) return null;
|
||||
return conversas.data.find((c: any) => c._id === conversaId);
|
||||
});
|
||||
|
||||
// Obter participantes para menções (apenas grupos e salas)
|
||||
const participantesParaMencoes = $derived(() => {
|
||||
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(() => {
|
||||
if (!mentionQuery.trim()) return participantesParaMencoes().slice(0, 5);
|
||||
const query = mentionQuery.toLowerCase();
|
||||
return participantesParaMencoes().filter((p: any) =>
|
||||
p.nome?.toLowerCase().includes(query) ||
|
||||
p.email?.toLowerCase().includes(query)
|
||||
).slice(0, 5);
|
||||
});
|
||||
|
||||
// Auto-resize do textarea e detectar menções
|
||||
function handleInput(e: Event) {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
|
||||
}
|
||||
|
||||
// Detectar menções (@)
|
||||
const cursorPos = target.selectionStart || 0;
|
||||
const textBeforeCursor = mensagem.substring(0, cursorPos);
|
||||
const lastAtIndex = textBeforeCursor.lastIndexOf('@');
|
||||
|
||||
if (lastAtIndex !== -1) {
|
||||
const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
|
||||
// Se não há espaço após o @, mostrar dropdown
|
||||
if (!textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
|
||||
mentionQuery = textAfterAt;
|
||||
mentionStartPos = lastAtIndex;
|
||||
showMentionsDropdown = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showMentionsDropdown = false;
|
||||
|
||||
// Indicador de digitação (debounce de 1s)
|
||||
if (digitacaoTimeout) {
|
||||
clearTimeout(digitacaoTimeout);
|
||||
@@ -55,15 +102,46 @@
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function inserirMencao(participante: any) {
|
||||
const nome = participante.nome.split(' ')[0]; // Usar primeiro nome
|
||||
const antes = mensagem.substring(0, mentionStartPos);
|
||||
const depois = mensagem.substring(textarea.selectionStart || mensagem.length);
|
||||
mensagem = antes + `@${nome} ` + depois;
|
||||
showMentionsDropdown = false;
|
||||
mentionQuery = "";
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
const newPos = antes.length + nome.length + 2;
|
||||
setTimeout(() => {
|
||||
textarea.setSelectionRange(newPos, newPos);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEnviar() {
|
||||
const texto = mensagem.trim();
|
||||
if (!texto || enviando) return;
|
||||
|
||||
// Extrair menções do texto (@nome)
|
||||
const mencoesIds: Id<"usuarios">[] = [];
|
||||
const mentionRegex = /@(\w+)/g;
|
||||
let match;
|
||||
while ((match = mentionRegex.exec(texto)) !== null) {
|
||||
const nomeMencionado = match[1];
|
||||
const participante = participantesParaMencoes().find((p: any) =>
|
||||
p.nome.split(' ')[0].toLowerCase() === nomeMencionado.toLowerCase()
|
||||
);
|
||||
if (participante) {
|
||||
mencoesIds.push(participante._id);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📤 [MessageInput] Enviando mensagem:", {
|
||||
conversaId,
|
||||
conteudo: texto,
|
||||
tipo: "texto",
|
||||
respostaPara: mensagemRespondendo?.id,
|
||||
mencoes: mencoesIds,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -73,12 +151,15 @@
|
||||
conteudo: texto,
|
||||
tipo: "texto",
|
||||
respostaPara: mensagemRespondendo?.id,
|
||||
mencoes: mencoesIds.length > 0 ? mencoesIds : undefined,
|
||||
});
|
||||
|
||||
console.log("✅ [MessageInput] Mensagem enviada com sucesso! ID:", result);
|
||||
|
||||
mensagem = "";
|
||||
mensagemRespondendo = null;
|
||||
showMentionsDropdown = false;
|
||||
mentionQuery = "";
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
}
|
||||
@@ -119,6 +200,22 @@
|
||||
});
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Navegar dropdown de menções
|
||||
if (showMentionsDropdown && participantesFiltrados().length > 0) {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// Implementação simples: selecionar primeiro participante
|
||||
if (e.key === "Enter") {
|
||||
inserirMencao(participantesFiltrados()[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
showMentionsDropdown = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Enter sem Shift = enviar
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -295,11 +392,36 @@
|
||||
bind:value={mensagem}
|
||||
oninput={handleInput}
|
||||
onkeydown={handleKeyDown}
|
||||
placeholder="Digite uma mensagem..."
|
||||
placeholder="Digite uma mensagem... (use @ para mencionar)"
|
||||
class="textarea textarea-bordered w-full resize-none min-h-[44px] max-h-[120px] pr-10"
|
||||
rows="1"
|
||||
disabled={enviando || uploadingFile}
|
||||
></textarea>
|
||||
|
||||
<!-- Dropdown de Menções -->
|
||||
{#if showMentionsDropdown && participantesFiltrados().length > 0 && (conversa()?.tipo === "grupo" || conversa()?.tipo === "sala_reuniao")}
|
||||
<div class="absolute bottom-full left-0 mb-2 bg-base-100 rounded-lg shadow-xl border border-base-300 z-50 w-64 max-h-48 overflow-y-auto">
|
||||
{#each participantesFiltrados() as participante (participante._id)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-2 hover:bg-base-200 transition-colors flex items-center gap-2"
|
||||
onclick={() => inserirMencao(participante)}
|
||||
>
|
||||
<div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center overflow-hidden">
|
||||
{#if participante.fotoPerfilUrl}
|
||||
<img src={participante.fotoPerfilUrl} alt={participante.nome} class="w-full h-full object-cover" />
|
||||
{:else}
|
||||
<span class="text-xs font-semibold">{participante.nome.charAt(0).toUpperCase()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium truncate">{participante.nome}</p>
|
||||
<p class="text-xs text-base-content/60 truncate">@{participante.nome.split(' ')[0]}</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Botão de enviar MODERNO -->
|
||||
|
||||
Reference in New Issue
Block a user