refactor(call): move noise-suppression toggle out of the call bar

Default flipped to off so voice doesn't get coloured by browser NS on
first run. Users turn it on explicitly under Settings → Sprache, where
the toggle already lived before the in-call button shipped.

The in-call SparklesIcon button and all the associated wiring
(onToggleNoiseSuppression, noiseSuppression prop, local subscription
in InCallPanel) is removed. The hot-swap UX is preserved: a new
subscribeAudioSettings watcher in CallContext detects noiseSuppression
flips during an active call and re-runs setupMicPipeline so the change
takes effect without rejoining.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-04-22 20:14:57 +02:00
parent eb8f702576
commit 02ca3e3581
4 changed files with 21 additions and 45 deletions
@@ -9,7 +9,6 @@ import {
MonitorStopIcon,
MusicIcon,
PhoneOffIcon,
SparklesIcon,
UsersIcon,
VideoIcon,
} from './icons';
@@ -19,7 +18,6 @@ interface Props {
sharing: boolean;
video: boolean;
deafened: boolean;
noiseSuppression?: boolean;
onToggleMute: () => void;
onToggleShare: () => void;
/** Right-click on the share button opens the quality picker dialog while
@@ -28,7 +26,6 @@ interface Props {
onShareContextMenu?: (e: React.MouseEvent) => void;
onToggleVideo?: () => void;
onToggleDeafen: () => void;
onToggleNoiseSuppression?: () => void;
onHangup: () => void;
onOpenParticipants?: () => void;
/** Toggle the in-call soundboard popover. Active = panel currently open. */
@@ -48,13 +45,11 @@ export function CallControls({
sharing,
video,
deafened,
noiseSuppression,
onToggleMute,
onToggleShare,
onShareContextMenu,
onToggleVideo,
onToggleDeafen,
onToggleNoiseSuppression,
onHangup,
onOpenParticipants,
onToggleSoundboard,
@@ -134,26 +129,6 @@ export function CallControls({
<MonitorShareIcon className="h-5 w-5" />
)}
</CallButton>
{onToggleNoiseSuppression && (
<CallButton
label={
noiseSuppression
? t('app:call.ns_off', {
defaultValue: 'Rauschunterdrückung aus',
})
: t('app:call.ns_on', {
defaultValue: 'Rauschunterdrückung an',
})
}
active={noiseSuppression ?? false}
activeTone="accent"
onClick={onToggleNoiseSuppression}
glass={glass}
className={btnSize}
>
<SparklesIcon className="h-5 w-5" />
</CallButton>
)}
{onToggleSoundboard && (
<CallButton
label={t('app:soundboard.toggle', { defaultValue: 'Soundboard' })}
@@ -6,11 +6,6 @@ import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import { type CallMode, useCall } from '../context/CallContext';
import {
getAudioSettings,
subscribeAudioSettings,
updateAudioSettings,
} from '../lib/audioSettings';
import {
getPttSettings,
type PttSettings,
@@ -80,7 +75,6 @@ export function InCallPanel({ conversation }: Props) {
hangup,
setCallMode,
setFocusedId,
setAudioInputDevice,
micError,
clearMicError,
retryMic,
@@ -94,10 +88,6 @@ export function InCallPanel({ conversation }: Props) {
const [volumeMenu, setVolumeMenu] = useState<
{ userId: string; displayName: string; x: number; y: number } | null
>(null);
const [noiseSuppression, setNoiseSuppression] = useState<boolean>(
() => getAudioSettings().noiseSuppression,
);
useEffect(() => subscribeAudioSettings((s) => setNoiseSuppression(s.noiseSuppression)), []);
// 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
@@ -180,7 +170,6 @@ export function InCallPanel({ conversation }: Props) {
sharing={isScreenSharing}
video={isCameraEnabled}
deafened={isDeafened}
noiseSuppression={noiseSuppression}
onToggleMute={toggleMute}
// 1-click share uses last-saved preset + displaySurface. Right-click
// opens the quality picker for users who want to change settings
@@ -198,14 +187,6 @@ export function InCallPanel({ conversation }: Props) {
}}
onToggleVideo={() => void toggleCamera()}
onToggleDeafen={toggleDeafen}
// Hot-swap the mic track with new constraints by replaying the
// input-device switch with the same id. setAudioInputDevice re-reads
// audioSettings, so flipping noiseSuppression first is enough.
onToggleNoiseSuppression={() => {
const prev = getAudioSettings().noiseSuppression;
updateAudioSettings({ noiseSuppression: !prev });
void setAudioInputDevice(getAudioSettings().inputDeviceId);
}}
onOpenParticipants={() => setParticipantsOpen((v) => !v)}
participantsOpen={participantsOpen}
onToggleSoundboard={() => setSoundboardOpen((v) => !v)}
+17
View File
@@ -1235,6 +1235,23 @@ export function CallProvider({ children }: { children: ReactNode }) {
});
}, []);
// Hot-swap the mic track when the user flips noiseSuppression in Settings
// so the change takes effect without needing to rejoin the call. Tracks
// the previous value in a ref so we only re-acquire getUserMedia on
// actual transitions (avoid a rebuild on every unrelated settings save).
useEffect(() => {
let lastNs = getAudioSettings().noiseSuppression;
return subscribeAudioSettings((s) => {
if (s.noiseSuppression === lastNs) return;
lastNs = s.noiseSuppression;
const r = roomRef.current;
if (!r) return;
// setupMicPipeline re-reads audioSettings so the new NS constraint
// gets picked up. Same helper as the initial join + retry paths.
void setupMicPipeline(r);
});
}, [setupMicPipeline]);
// --- Push-to-talk ------------------------------------------------------
// While PTT is active + we're in a connected call, the mic is held off
// except while the configured key is pressed. Under Tauri we also register
+4 -1
View File
@@ -41,7 +41,10 @@ const DEFAULTS: AudioSettings = {
inputDeviceId: null,
outputDeviceId: null,
voiceThreshold: 0.03,
noiseSuppression: true,
// Off by default — browser-native NS colours voice audibly on some
// mics and is a frequent "why does my voice sound weird" report.
// Users who want it enable it explicitly in Settings → Sprache.
noiseSuppression: false,
videoBackgroundBlur: false,
ringtoneVolume: 0.9,
};