CreaRack-SL

Chat equipo: avisos audio+badge, maximizar, paste imágenes y Jitsi Meet

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) en document.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 como data:image/png en el <link rel="icon">.
  • Chip rojo N sin leer visible 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 localStorage con la clave workspace:chat:lastSeenId para guardar el id má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 focus o document visibilitychange → visible), se llama a markAllAsRead() que limpia badge, título y chip.
  • El primer fetch no dispara aviso (evita el beep molesto al cargar la página): initialFetchDoneRef actúa como guard.

Archivos clave

ArchivoRol
src/lib/chatNotify.tsplayChatBeep(), setTabBadge(n), setFaviconBadge(active)
src/components/widgets/TeamChat.tsxConsume 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: boolean gestionado en DesktopA y pasado al componente como prop maximized.
  • 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 maximized se 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

  1. Usuario presiona Ctrl+V con una imagen en el portapapeles (p. ej. captura de pantalla).
  2. onPaste detecta el File de tipo imagen, llama a POST /api/chat/upload-image con multipart/form-data.
  3. El CF Pages Function sube el archivo al bucket R2 TASK_ATTACHMENTS con el prefijo chat/ y devuelve { filename, url, content_type }.
  4. Se inserta en el draft el marcador [abrir imagen:/api/chat-images/<uuid.ext>].
  5. La función renderBody() detecta el marcador con CHAT_IMG_REGEX y lo convierte en un link <abrir imagen> que abre la imagen en pestaña nueva.

Restricciones del endpoint

ParámetroValor
Tamaño máximo5 MB
MIME permitidosimage/png, image/jpeg, image/webp, image/gif
Bucket R2TASK_ATTACHMENTS
Prefijo R2chat/
AuthCF 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-data con campo file.
  • Valida MIME y tamaño. Genera UUID + extensión como nombre del objeto R2.
  • Devuelve 201 con { 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]]