feat: implement audio/video call functionality in chat

- Added a new schema for managing audio/video calls, including fields for call type, room name, and participant management.
- Enhanced ChatWindow component to support initiating audio and video calls with dynamic loading of the CallWindow component.
- Updated package dependencies to include 'lib-jitsi-meet' for call handling.
- Refactored existing code to accommodate new call features and improve user experience.
This commit is contained in:
2025-11-21 13:17:44 -03:00
parent bc1e08914b
commit 2792424454
15 changed files with 3986 additions and 3 deletions

View File

@@ -10,7 +10,20 @@
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import {
Bell,
X,
ArrowLeft,
LogOut,
MoreVertical,
Users,
Clock,
XCircle,
Phone,
Video
} from 'lucide-svelte';
interface Props {
conversaId: string;
@@ -26,11 +39,20 @@
let showSalaManager = $state(false);
let showAdminMenu = $state(false);
let showNotificacaoModal = $state(false);
let iniciandoChamada = $state(false);
// Importação dinâmica do CallWindow apenas no cliente
let CallWindowComponent: any = $state(null);
const chamadaAtual = $derived(chamadaAtivaQuery?.data);
const conversas = useQuery(api.chat.listarConversas, {});
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, {
conversaId: conversaId as Id<'conversas'>
});
const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, {
conversaId: conversaId as Id<'conversas'>
});
const conversa = $derived(() => {
console.log('🔍 [ChatWindow] Buscando conversa ID:', conversaId);
@@ -115,6 +137,54 @@
alert(errorMessage);
}
}
// Funções para chamadas
async function iniciarChamada(tipo: 'audio' | 'video'): Promise<void> {
if (chamadaAtual) {
alert('Já existe uma chamada ativa nesta conversa');
return;
}
try {
iniciandoChamada = true;
const chamadaId = await client.mutation(api.chamadas.criarChamada, {
conversaId: conversaId as Id<'conversas'>,
tipo,
audioHabilitado: true,
videoHabilitado: tipo === 'video'
});
chamadaAtiva = chamadaId;
} catch (error) {
console.error('Erro ao iniciar chamada:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao iniciar chamada';
alert(errorMessage);
} finally {
iniciandoChamada = false;
}
}
function fecharChamada(): void {
chamadaAtiva = null;
}
// Carregar CallWindow dinamicamente apenas no cliente
onMount(async () => {
if (browser && !CallWindowComponent) {
try {
const module = await import('../call/CallWindow.svelte');
CallWindowComponent = module.default;
} catch (error) {
console.error('Erro ao carregar CallWindow:', error);
}
}
});
// Verificar se usuário é anfitrião da chamada atual
const meuPerfil = useQuery(api.auth.getCurrentUser, {});
const souAnfitriao = $derived(
chamadaAtual && meuPerfil?.data ? chamadaAtual.criadoPor === meuPerfil.data._id : false
);
</script>
<div class="flex h-full flex-col" onclick={() => (showAdminMenu = false)}>
@@ -233,6 +303,36 @@
<!-- Botões de ação -->
<div class="flex items-center gap-1">
<!-- Botões de Chamada -->
{#if !chamadaAtual}
<button
type="button"
class="btn btn-sm btn-circle btn-primary"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('audio');
}}
disabled={iniciandoChamada}
aria-label="Ligação de áudio"
title="Iniciar ligação de áudio"
>
<Phone class="h-5 w-5" strokeWidth={2} />
</button>
<button
type="button"
class="btn btn-sm btn-circle btn-primary"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('video');
}}
disabled={iniciandoChamada}
aria-label="Ligação de vídeo"
title="Iniciar ligação de vídeo"
>
<Video class="h-5 w-5" strokeWidth={2} />
</button>
{/if}
<!-- Botão Sair (apenas para grupos e salas de reunião) -->
{#if conversa() && (conversa()?.tipo === 'grupo' || conversa()?.tipo === 'sala_reuniao')}
<button
@@ -400,6 +500,21 @@
/>
{/if}
<!-- Janela de Chamada -->
{#if browser && chamadaAtiva && chamadaAtual && CallWindowComponent}
<div class="pointer-events-none fixed inset-0 z-[9999]">
{@const Component = CallWindowComponent}
<Component
chamadaId={chamadaAtiva}
conversaId={conversaId as Id<'conversas'>}
tipo={chamadaAtual.tipo}
roomName={chamadaAtual.roomName}
ehAnfitriao={souAnfitriao}
onClose={fecharChamada}
/>
</div>
{/if}
<!-- Modal de Enviar Notificação -->
{#if showNotificacaoModal && conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
<dialog