feat: implement comprehensive chat system with user presence management, notification handling, and avatar integration; enhance UI components for improved user experience

This commit is contained in:
2025-10-28 11:57:54 -03:00
parent 81e6eb4a42
commit ee2c9c3ae0
47 changed files with 8274 additions and 195 deletions

View File

@@ -0,0 +1,208 @@
<script lang="ts">
import { useConvexClient } 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";
interface Props {
conversaId: Id<"conversas">;
}
let { conversaId }: Props = $props();
const client = useConvexClient();
let mensagem = $state("");
let textarea: HTMLTextAreaElement;
let enviando = $state(false);
let uploadingFile = $state(false);
let digitacaoTimeout: ReturnType<typeof setTimeout> | null = null;
// Auto-resize do textarea
function handleInput() {
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
}
// Indicador de digitação (debounce de 1s)
if (digitacaoTimeout) {
clearTimeout(digitacaoTimeout);
}
digitacaoTimeout = setTimeout(() => {
if (mensagem.trim()) {
client.mutation(api.chat.indicarDigitacao, { conversaId });
}
}, 1000);
}
async function handleEnviar() {
const texto = mensagem.trim();
if (!texto || enviando) return;
try {
enviando = true;
await client.mutation(api.chat.enviarMensagem, {
conversaId,
conteudo: texto,
tipo: "texto",
});
mensagem = "";
if (textarea) {
textarea.style.height = "auto";
}
} catch (error) {
console.error("Erro ao enviar mensagem:", error);
alert("Erro ao enviar mensagem");
} finally {
enviando = false;
}
}
function handleKeyDown(e: KeyboardEvent) {
// Enter sem Shift = enviar
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleEnviar();
}
}
async function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
// Validar tamanho (max 10MB)
if (file.size > 10 * 1024 * 1024) {
alert("Arquivo muito grande. O tamanho máximo é 10MB.");
return;
}
try {
uploadingFile = true;
// 1. Obter upload URL
const uploadUrl = await client.mutation(api.chat.uploadArquivoChat, { conversaId });
// 2. Upload do arquivo
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
if (!result.ok) {
throw new Error("Falha no upload");
}
const { storageId } = await result.json();
// 3. Enviar mensagem com o arquivo
const tipo = file.type.startsWith("image/") ? "imagem" : "arquivo";
await client.mutation(api.chat.enviarMensagem, {
conversaId,
conteudo: tipo === "imagem" ? "" : file.name,
tipo: tipo as any,
arquivoId: storageId,
arquivoNome: file.name,
arquivoTamanho: file.size,
arquivoTipo: file.type,
});
// Limpar input
input.value = "";
} catch (error) {
console.error("Erro ao fazer upload:", error);
alert("Erro ao enviar arquivo");
} finally {
uploadingFile = false;
}
}
onMount(() => {
if (textarea) {
textarea.focus();
}
});
</script>
<div class="p-4">
<div class="flex items-end gap-2">
<!-- Botão de anexar arquivo -->
<label class="btn btn-ghost btn-sm btn-circle flex-shrink-0">
<input
type="file"
class="hidden"
onchange={handleFileUpload}
disabled={uploadingFile || enviando}
accept="*/*"
/>
{#if uploadingFile}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<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="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13"
/>
</svg>
{/if}
</label>
<!-- Textarea -->
<div class="flex-1 relative">
<textarea
bind:this={textarea}
bind:value={mensagem}
oninput={handleInput}
onkeydown={handleKeyDown}
placeholder="Digite uma mensagem..."
class="textarea textarea-bordered w-full resize-none min-h-[44px] max-h-[120px] pr-10"
rows="1"
disabled={enviando || uploadingFile}
></textarea>
</div>
<!-- Botão de enviar -->
<button
type="button"
class="btn btn-primary btn-circle flex-shrink-0"
onclick={handleEnviar}
disabled={!mensagem.trim() || enviando || uploadingFile}
aria-label="Enviar"
>
{#if enviando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<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 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
/>
</svg>
{/if}
</button>
</div>
<!-- Informação sobre atalhos -->
<p class="text-xs text-base-content/50 mt-2 text-center">
Pressione Enter para enviar, Shift+Enter para quebrar linha
</p>
</div>