Chat del equipo — Avisos audio+badge, maximizar, paste imágenes y Jitsi (PR#58)
Chat del equipo — Mejoras s77
Cuatro mejoras del widget TeamChat del dashboard interno (DesktopA), implementadas en PR#58 por petición de Edu. El widget vive en src/components/widgets/TeamChat.tsx y se embebe en la variante de layout src/components/variants/desktop/DesktopA.tsx.
1 · Avisos audio + badge de mensajes no leídos
Cuando llega un mensaje de otro miembro del equipo y el tab está oculto (document.visibilityState !== 'visible'), el sistema dispara tres tipos de aviso coordinados:
| Canal | Mecanismo |
|---|---|
| Audio | Beep de dos tonos «ding» (880 Hz → 1100 Hz, ~320 ms) vía Web Audio API. Solo si el tab está oculto. |
| Título de pestaña | (N) Workspace — prefijo con el contador de no leídos. Se resetea al recuperar el foco. |
| Favicon | Punto rojo dibujado en canvas sobre el favicon original. Fallback a círculo rojo entero si hay error CORS. |
| Chip interno | Chip rojo «N sin leer» visible dentro del propio widget mientras unreadCount > 0. |
La lógica reside en src/lib/chatNotify.ts (funciones playChatBeep, setTabBadge, setFaviconBadge).
Persistencia: el ID del último mensaje visto (lastSeenId) se guarda en localStorage bajo la clave workspace:chat:lastSeenId. Se actualiza al recuperar foco o visibilidad, y se carga al montar el componente para distinguir mensajes realmente nuevos de los que ya se habían leído en sesiones anteriores.
El primer fetch no dispara aviso: initialFetchDoneRef asegura que al cargar la página no suene el beep por mensajes históricos.
2 · Maximizar widget
El bento del chat soporta un estado maximized que lo saca del grid del dashboard mediante position: fixed:
.team-chat-bento.maximized {
position: fixed;
top: 76px;
right: 24px;
bottom: 24px;
width: clamp(360px, 34vw, 520px);
z-index: 60;
box-shadow: 0 20px 60px rgba(0,0,0,0.45);
max-height: calc(100vh - 100px);
}
z-index: 60— flota sobre el grid pero por debajo de modales (z-index: 1000).- El estado
chatMaximizedvive enDesktopA(prop drilling haciaTeamChat). - El grid del resto del dashboard no se altera: el elemento sigue ocupando su celda en el flujo normal, pero visualmente queda fixed en la esquina derecha.
- Botón
⛶para maximizar /⊟para restaurar.
3 · Pegar capturas con Ctrl+V
El textarea del chat acepta imágenes del portapapeles. Flujo:
onPaste → detecta image/* en clipboardData.items
→ e.preventDefault()
→ POST /api/chat/upload-image (multipart, field: "file")
→ responde { filename, url, content_type }
→ inserta marcador [abrir imagen:<url>] en el draft
En el render, CHAT_IMG_REGEX detecta el marcador y lo reemplaza con un link <abrir imagen> (cyan, JetBrains Mono, target="_blank"). Las URLs sueltas siguen siendo autolinkificadas por URL_REGEX, y ambos tokenizadores coexisten sin duplicar matches.
Restricciones del endpoint de upload:
- Tamaño máximo: 5 MB.
- MIME permitidos:
image/png,image/jpeg,image/webp,image/gif. - Storage: R2 bucket
TASK_ATTACHMENTS, prefixchat/, nombre =<uuid>.<ext>. - Sin tabla D1 intermedia: el objeto R2 es la fuente de verdad. Imágenes de mensajes borrados quedan huérfanas (aceptado como deuda técnica mínima).
4 · Reunión de voz/vídeo con Jitsi Meet
Botón «Reunión» en la toolbar interna del widget abre JitsiMeetingModal, un portal React renderizado en document.body.
| Parámetro | Valor |
|---|---|
| Servidor | meet.jit.si (infraestructura pública Jitsi, gratuita) |
| Sala | CrearackSL-Workspace-EduDaniTxell-Sala (fija, sin cuenta) |
| Permisos iframe | camera; microphone; fullscreen; display-capture; autoplay |
| Capacidades | Voz + vídeo + screen share + chat interno Jitsi |
- Sala persistente: quien entra primero la crea; los demás se unen al mismo nombre.
config.prejoinPageEnabled=trueen la URL del iframe activa la pantalla de antesala de Jitsi.- Cierre con
Esc(listener endocument) o botón «Cerrar». - Botón «Abrir aparte» abre la sala en pestaña nueva (
target="_blank"). - 0 infraestructura propia: no hay servidor WebRTC, no hay TURN/STUN propios.
Decisiones de diseño
- Beep solo si tab oculta: evita ruido mientras el usuario ya está mirando el chat.
Page Visibility API+window focusevent. - Badge favicon en canvas: no requiere favicon adicional; se pinta dinámicamente. Fallback defensivo ante errores CORS del
<img>original. - Maximizar vía
position: fixed: mantiene el grid del dashboard intacto. Alternativa descartada: redimensionar la columna del grid (implica reflow en todos los bentos). - Sin metadata en D1 para chat-images: el R2 key es la URL servible. Deuda técnica aceptada: si el volumen crece, se puede añadir cleanup periódico vía Worker scheduled.
- Jitsi sin WebRTC nativo: 0 infra, 0 signup, lista en minutos. Limitación: depende de la disponibilidad del servidor público
meet.jit.si.
Archivos clave
| Archivo | Rol |
|---|---|
src/lib/chatNotify.ts | Utilidades audio+badge (playChatBeep, setTabBadge, setFaviconBadge) |
src/components/widgets/TeamChat.tsx | Widget principal — integra las 4 mejoras |
src/components/widgets/JitsiMeetingModal.tsx | Modal iframe Jitsi (portal React) |
src/components/variants/desktop/DesktopA.tsx | Layout host — estado chatMaximized |
functions/api/chat/upload-image.ts | CF Pages Function — POST upload a R2 |
functions/api/chat-images/[name].ts | CF Pages Function — GET stream desde R2 |
src/styles/globals.css | .team-chat-bento.maximized — CSS fixed overlay |
Véase también
- [[entity—chat—service—chatnotify]]
- [[entity—chat—endpoint—upload-image]]