mirror of
https://github.com/temetro/temetro.git
synced 2026-08-09 18:20:00 +00:00
913a217e1d
- Live invite: ring a clinic member into a room (socket call:invite + a bell notification); the invitee gets a toast with a Join action. Notifications of type "meeting" deep-link to /messages/meetings?room=, which auto-joins. - Scheduling: new scheduled_meetings table + /api/meetings/events (list mine, create, delete); a Calendar tab on the Meetings page with a month picker (meeting-day dots), the day's agenda, and a Schedule-meeting dialog (title/date/time/participants). - Redesign: rounded control bar with tooltips (mic/cam/screen/invite + separated red Leave), speaking ring on tiles (Web Audio), and live room-occupancy counts via call:presence broadcasts. Migration 0025. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
47 lines
1.4 KiB
TypeScript
47 lines
1.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
// Returns true while the given stream's audio is above a speaking threshold —
|
|
// used to draw a Discord-style "speaking" ring around a participant's tile.
|
|
// Degrades silently if the Web Audio API isn't available.
|
|
export function useSpeaking(stream: MediaStream | null): boolean {
|
|
const [speaking, setSpeaking] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!stream || stream.getAudioTracks().length === 0) {
|
|
setSpeaking(false);
|
|
return;
|
|
}
|
|
let ctx: AudioContext | null = null;
|
|
let raf = 0;
|
|
try {
|
|
ctx = new AudioContext();
|
|
const source = ctx.createMediaStreamSource(stream);
|
|
const analyser = ctx.createAnalyser();
|
|
analyser.fftSize = 512;
|
|
source.connect(analyser);
|
|
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
const tick = () => {
|
|
analyser.getByteTimeDomainData(data);
|
|
let sum = 0;
|
|
for (let i = 0; i < data.length; i++) {
|
|
const v = (data[i]! - 128) / 128;
|
|
sum += v * v;
|
|
}
|
|
setSpeaking(Math.sqrt(sum / data.length) > 0.045);
|
|
raf = requestAnimationFrame(tick);
|
|
};
|
|
tick();
|
|
} catch {
|
|
/* no Web Audio — no ring */
|
|
}
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
ctx?.close().catch(() => {});
|
|
};
|
|
}, [stream]);
|
|
|
|
return speaking;
|
|
}
|