UX: notificaciones de chat con toast y sonido (WebAudio) + skeletons de carga en Agenda y Chat
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import type { FC, FormEvent, KeyboardEvent } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Send, Search, ArrowLeft } from 'lucide-react';
|
||||
import Layout from '../components/Layout';
|
||||
import { Card, Button, EmptyState, toast } from '../components/ui';
|
||||
import { Card, Button, EmptyState, Skeleton, toast } from '../components/ui';
|
||||
import { ROLE_LABELS } from '../lib/auth';import { odooApi, type ChatUser, type ChatMessage } from '../services/odoo';
|
||||
import { setActiveChatUser } from '../lib/chat-notifications';
|
||||
|
||||
const POLL_MS = 5000;
|
||||
|
||||
@@ -38,6 +40,7 @@ const Chat: FC = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const lastIdRef = useRef(0);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const activeUser = users.find((u) => u.id === activeId) || null;
|
||||
|
||||
@@ -93,6 +96,21 @@ const Chat: FC = () => {
|
||||
return () => clearInterval(id);
|
||||
}, [fetchUsers, fetchThread, activeId]);
|
||||
|
||||
// Registrar la conversación visible: las notificaciones globales no suenan
|
||||
// ni muestran toast para el remitente que ya está en pantalla
|
||||
useEffect(() => {
|
||||
setActiveChatUser(activeId);
|
||||
return () => setActiveChatUser(null);
|
||||
}, [activeId]);
|
||||
|
||||
// Abrir conversación directa por query (?with=<userId>), ej. desde el toast
|
||||
useEffect(() => {
|
||||
const withId = parseInt(searchParams.get('with') || '', 10);
|
||||
if (!withId || withId === activeId) return;
|
||||
if (!users.some((u) => u.id === withId)) return;
|
||||
openConversation(withId);
|
||||
}, [searchParams, users, activeId, openConversation]);
|
||||
|
||||
const send = async (e?: FormEvent) => {
|
||||
e?.preventDefault();
|
||||
const body = draft.trim();
|
||||
@@ -146,7 +164,17 @@ const Chat: FC = () => {
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loadingUsers ? (
|
||||
<p className="p-4 text-sm text-theme-muted">Cargando...</p>
|
||||
<div className="p-3 space-y-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<Skeleton className="h-10 w-10 rounded-full shrink-0" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<Skeleton className="h-3.5 w-2/3" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<p className="p-4 text-sm text-theme-muted">No hay usuarios para mostrar.</p>
|
||||
) : (
|
||||
@@ -220,7 +248,13 @@ const Chat: FC = () => {
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-2 bg-theme-bg">
|
||||
{loadingThread ? (
|
||||
<p className="text-sm text-theme-muted text-center py-6">Cargando mensajes...</p>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className={`flex ${i % 2 ? 'justify-end' : 'justify-start'}`}>
|
||||
<Skeleton className={`h-9 ${i % 2 ? 'w-1/2' : 'w-2/3'} rounded-2xl`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<p className="text-sm text-theme-muted text-center py-6">
|
||||
Aún no hay mensajes. Escribe el primero.
|
||||
|
||||
Reference in New Issue
Block a user