Chat equipo: avisos audio+badge, maximizar, paste imágenes y Jitsi Meet
Cuatro mejoras del widget TeamChat del dashboard (DesktopA) mergeadas en el commit 605a339 (2026-05-20). Pedidas por Edu. Afectan a src/components/widgets/TeamChat.tsx, la librería src/lib/chatNotify.ts, los CF Pages Functions de upload/stream de imágenes y el nuevo modal JitsiMeetingModal.
Item 1 — Avisos audio + badge de mensajes no leídos
Cuando llega un mensaje de otro miembro del equipo y el tab no está visible, el sistema dispara:
- Beep WebAudio de dos tonos (880 Hz → 1100 Hz, 60 ms cada uno) generado en tiempo real con
AudioContext. Sin assets externos. - Badge
(N)endocument.title— prefija el título con(N)donde N es el número de mensajes sin leer. - Punto rojo en el favicon — dibujado sobre el favicon original en un
<canvas>32×32 y reinyectado comodata:image/pngen el<link rel="icon">. - Chip rojo
N sin leervisible en la barra interna del widget (por encima de la lista de mensajes).
Si el usuario está mirando el tab (visibilityState === 'visible'), los mensajes se marcan como leídos inmediatamente, sin emitir sonido ni badge.
Persistencia del estado de lectura
- Se usa
localStoragecon la claveworkspace:chat:lastSeenIdpara guardar elidmáximo de mensaje visto. - Al montar el componente se recupera ese valor: si hay mensajes más nuevos y el tab está oculto, se muestra el contador sin esperar al próximo poll.
- Al recibir focus (
window focusodocument visibilitychange → visible), se llama amarkAllAsRead()que limpia badge, título y chip. - El primer fetch no dispara aviso (evita el beep molesto al cargar la página):
initialFetchDoneRefactúa como guard.
Archivos clave
| Archivo | Rol |
|---|---|
src/lib/chatNotify.ts | playChatBeep(), setTabBadge(n), setFaviconBadge(active) |
src/components/widgets/TeamChat.tsx | Consume chatNotify; gestiona unreadCount, lastSeenIdRef, efectos de visibilidad |
Item 2 — Maximizar widget
El bento del chat (div.team-chat-bento) puede expandirse a posición fija cubriendo toda la altura disponible del canvas en el lado derecho del dashboard.
- Botón
⛶(esquina superior derecha del widget). Al activar:⊟para restaurar. - Estado
chatMaximized: booleangestionado enDesktopAy pasado al componente como propmaximized. - CSS en
globals.css:.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).- La clase
maximizedse aplica combinando el array de clases con.filter(Boolean).join(' ')para que Prettier no elimine el espacio.
Item 3 — Pegar capturas con Ctrl+V
El textarea del chat intercepta el evento paste buscando items image/* en el clipboardData.
Flujo
- Usuario presiona Ctrl+V con una imagen en el portapapeles (p. ej. captura de pantalla).
onPastedetecta elFilede tipo imagen, llama aPOST /api/chat/upload-imageconmultipart/form-data.- El CF Pages Function sube el archivo al bucket R2
TASK_ATTACHMENTScon el prefijochat/y devuelve{ filename, url, content_type }. - Se inserta en el draft el marcador
[abrir imagen:/api/chat-images/<uuid.ext>]. - La función
renderBody()detecta el marcador conCHAT_IMG_REGEXy lo convierte en un link<abrir imagen>que abre la imagen en pestaña nueva.
Restricciones del endpoint
| Parámetro | Valor |
|---|---|
| Tamaño máximo | 5 MB |
| MIME permitidos | image/png, image/jpeg, image/webp, image/gif |
| Bucket R2 | TASK_ATTACHMENTS |
| Prefijo R2 | chat/ |
| Auth | CF Access (cualquier sesión staff válida) |
Durante la subida el textarea muestra placeholder Subiendo imagen… y queda deshabilitado.
Item 4 — Jitsi Meet embebido
Botón “Reunión” en la barra interna del widget abre JitsiMeetingModal: un portal React con createPortal que renderiza un <iframe> fullscreen a meet.jit.si.
- Sin cuenta, sin signup. El servidor público de Jitsi es gratuito.
- Sala fija del equipo:
CrearackSL-Workspace-EduDaniTxell-Sala. El primer miembro que entra la crea; los demás se unen automáticamente. - Permite: voz, vídeo, screen share y chat de Jitsi.
- El modal puede cerrarse con Esc o con el botón “Cerrar”.
- El botón “Abrir aparte” abre la sala en una pestaña nueva (
target="_blank"). allow="camera; microphone; fullscreen; display-capture; autoplay"en el<iframe>.document.body.style.overflow = 'hidden'mientras el modal está abierto (se restaura al cerrar).
Endpoints CF Pages Functions nuevos
POST /api/chat/upload-image
- Acepta
multipart/form-datacon campofile. - Valida MIME y tamaño. Genera UUID + extensión como nombre del objeto R2.
- Devuelve
201con{ filename, url, content_type }. - Errores:
400(form inválido),413(demasiado grande),415(MIME no permitido).
GET /api/chat-images/:name
- Streama el objeto R2
chat/<name>como respuesta HTTP. - Valida el nombre con regex
^[A-Za-z0-9._-]+$para evitar path traversal. - Headers:
Content-Type,Content-Length,Content-Disposition: inline,Cache-Control: private, max-age=3600. - Errores:
400(nombre inválido),404(no encontrado en R2).
Véase también
- [[entity—chat—service—chatnotify]]
- [[entity—chat—endpoint—upload-image]]
- [[entity—chat—endpoint—chat-images]]
- [[entity—chat—widget—jitsi-meeting-modal]]
- [[workspace—dashboard]]