CreaRack-SL

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:

CanalMecanismo
AudioBeep 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.
FaviconPunto rojo dibujado en canvas sobre el favicon original. Fallback a círculo rojo entero si hay error CORS.
Chip internoChip 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 chatMaximized vive en DesktopA (prop drilling hacia TeamChat).
  • 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, prefix chat/, 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ámetroValor
Servidormeet.jit.si (infraestructura pública Jitsi, gratuita)
SalaCrearackSL-Workspace-EduDaniTxell-Sala (fija, sin cuenta)
Permisos iframecamera; microphone; fullscreen; display-capture; autoplay
CapacidadesVoz + vídeo + screen share + chat interno Jitsi
  • Sala persistente: quien entra primero la crea; los demás se unen al mismo nombre.
  • config.prejoinPageEnabled=true en la URL del iframe activa la pantalla de antesala de Jitsi.
  • Cierre con Esc (listener en document) 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 focus event.
  • 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

ArchivoRol
src/lib/chatNotify.tsUtilidades audio+badge (playChatBeep, setTabBadge, setFaviconBadge)
src/components/widgets/TeamChat.tsxWidget principal — integra las 4 mejoras
src/components/widgets/JitsiMeetingModal.tsxModal iframe Jitsi (portal React)
src/components/variants/desktop/DesktopA.tsxLayout host — estado chatMaximized
functions/api/chat/upload-image.tsCF Pages Function — POST upload a R2
functions/api/chat-images/[name].tsCF 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]]