feat(call): hide soundboard button when the user has no sounds

Previously the music icon always rendered in the in-call bar, opening a
popover with a "Keine Sounds gespeichert" empty-state. Matches Discord's
pattern better to just drop the button entirely until the user has
something to play — otherwise it reads as a broken control. Live-
subscribes to soundboardStorage so adding/removing sounds mid-call flips
the button in or out without reopening the call. Also closes the popover
automatically if the user clears their last sound while it's open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-04-22 21:00:33 +02:00
parent b44a785d20
commit 7ad8ba82b6
+45 -2
View File
@@ -11,6 +11,10 @@ import {
type PttSettings,
subscribePttSettings,
} from '../lib/pttSettings';
import {
listSounds,
subscribeSoundboardChanges,
} from '../lib/soundboardStorage';
import { useActiveSpeakers } from '../lib/useActiveSpeakers';
import { CallControls } from './CallControls';
import { CallParticipantTile } from './CallParticipantTile';
@@ -93,6 +97,37 @@ export function InCallPanel({ conversation }: Props) {
const [shareMenu, setShareMenu] = useState<
{ userId: string; displayName: string; hasAudio: boolean; x: number; y: number } | null
>(null);
// Soundboard-count so the in-call bar only surfaces the music button when
// the user actually has something to play. Matches Discord's "hide soundboard
// when empty" behaviour — no point dangling a button that opens to a blank
// "Keine Sounds" popover. Subscribes live so a sound added mid-call makes
// the button pop in without reopening the call.
const [soundboardCount, setSoundboardCount] = useState<number | null>(null);
useEffect(() => {
let cancelled = false;
const refresh = async () => {
try {
const all = await listSounds();
if (!cancelled) setSoundboardCount(all.length);
} catch {
if (!cancelled) setSoundboardCount(0);
}
};
void refresh();
const unsub = subscribeSoundboardChanges(() => {
void refresh();
});
return () => {
cancelled = true;
unsub();
};
}, []);
// Close the popover if the user just cleared their last sound while it was
// open — keeps the panel from lingering over an empty list.
useEffect(() => {
if (soundboardCount === 0) setSoundboardOpen(false);
}, [soundboardCount]);
// Active-speaker auto-focus uses "who most recently started speaking"
// rather than "exactly one speaker" — matches Discord more closely and
// handles the case where two people talk briefly without the focus
@@ -230,8 +265,16 @@ export function InCallPanel({ conversation }: Props) {
onToggleDeafen={toggleDeafen}
onOpenParticipants={() => setParticipantsOpen((v) => !v)}
participantsOpen={participantsOpen}
onToggleSoundboard={() => setSoundboardOpen((v) => !v)}
soundboardOpen={soundboardOpen}
// Soundboard-Button nur wenn mind. ein Sound existiert. Bis der Count
// aus IndexedDB geladen ist (null), auch nicht rendern — verhindert
// einen Flash des Buttons beim Call-Start wenn der User eh keine
// Sounds hat.
{...(soundboardCount && soundboardCount > 0
? {
onToggleSoundboard: () => setSoundboardOpen((v) => !v),
soundboardOpen,
}
: {})}
onHangup={() => void hangup()}
compact={callMode !== 'fullscreen'}
glass={callMode === 'fullscreen'}