feat(call): Discord-style polish pass (groups A-C)
A — Call core: - Deafen now implies mute + remembers pre-deafen mic state so un-deafen restores it (Discord-parity). Peers still see the headphones-off + mic-off badges in sync via the existing data-channel broadcast. - Self-join sound fires on the local peer's r.connect() too, not just on remote ParticipantConnected, so the user gets the "I'm in" cue. - New CallState.reconnecting holds the UI steady when LiveKit drops the signaling socket and retries; duration keeps ticking, status label switches to "Verbinde neu…". Full teardown only on terminal Disconnected (after LK gives up). - joinActiveCall falls back to connected after 5s if no peer arrived — avoids hanging in "Verbinde…" when peers left the room mid-rejoin. B — Ringtone: - Oscillator base gain up (incoming 0.22 -> 0.4, outgoing 0.14 -> 0.22) so the default pattern survives laptop speakers + background music. - New ringtoneVolume slider in Settings, default 0.9, live-applies to both the oscillator fallback and the custom-file <audio> element. C — Participant tile: - Split the speaking indicator: video tiles get the emerald border + inset glow; audio tiles rely on the existing avatar pulse. No more double-chrome when someone talks in grid/focus view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
getAudioSettings,
|
||||
subscribeAudioSettings,
|
||||
updateAudioSettings,
|
||||
} from '../lib/audioSettings';
|
||||
import {
|
||||
clearIncomingRingtone,
|
||||
getIncomingRingtone,
|
||||
@@ -30,6 +35,10 @@ export function RingtoneSettings({ disabled = false }: Props) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [volume, setVolume] = useState<number>(() => getAudioSettings().ringtoneVolume);
|
||||
|
||||
// Subscribe so cross-tab / in-call slider moves stay in sync here too.
|
||||
useEffect(() => subscribeAudioSettings((s) => setVolume(s.ringtoneVolume)), []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
@@ -128,7 +137,7 @@ export function RingtoneSettings({ disabled = false }: Props) {
|
||||
const url = URL.createObjectURL(current.blob);
|
||||
const el = new Audio(url);
|
||||
el.loop = false;
|
||||
el.volume = 0.85;
|
||||
el.volume = volume;
|
||||
el.onended = () => stopPreview();
|
||||
el.onerror = () => {
|
||||
setError(
|
||||
@@ -233,6 +242,36 @@ export function RingtoneSettings({ disabled = false }: Props) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<label
|
||||
htmlFor="ringtone-volume"
|
||||
className="shrink-0 text-xs font-medium text-fg-muted"
|
||||
>
|
||||
{t('app:settings.ringtone_volume', { defaultValue: 'Lautstärke' })}
|
||||
</label>
|
||||
<input
|
||||
id="ringtone-volume"
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={volume}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
setVolume(v);
|
||||
updateAudioSettings({ ringtoneVolume: v });
|
||||
// Apply to the currently-playing preview so the user hears the
|
||||
// slider effect immediately while dragging.
|
||||
if (previewRef.current) previewRef.current.volume = v;
|
||||
}}
|
||||
aria-label={t('app:settings.ringtone_volume', { defaultValue: 'Lautstärke' })}
|
||||
className="flex-1 accent-accent"
|
||||
/>
|
||||
<span className="w-10 shrink-0 text-right text-xs tabular-nums text-fg-muted">
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-fg-muted">
|
||||
{t('app:settings.ringtone_hint', {
|
||||
defaultValue:
|
||||
|
||||
Reference in New Issue
Block a user