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:
@@ -9,7 +9,6 @@ import {
|
|||||||
MonitorStopIcon,
|
MonitorStopIcon,
|
||||||
MusicIcon,
|
MusicIcon,
|
||||||
PhoneOffIcon,
|
PhoneOffIcon,
|
||||||
SparklesIcon,
|
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
VideoIcon,
|
VideoIcon,
|
||||||
} from './icons';
|
} from './icons';
|
||||||
@@ -19,7 +18,6 @@ interface Props {
|
|||||||
sharing: boolean;
|
sharing: boolean;
|
||||||
video: boolean;
|
video: boolean;
|
||||||
deafened: boolean;
|
deafened: boolean;
|
||||||
noiseSuppression?: boolean;
|
|
||||||
onToggleMute: () => void;
|
onToggleMute: () => void;
|
||||||
onToggleShare: () => void;
|
onToggleShare: () => void;
|
||||||
/** Right-click on the share button opens the quality picker dialog while
|
/** Right-click on the share button opens the quality picker dialog while
|
||||||
@@ -28,7 +26,6 @@ interface Props {
|
|||||||
onShareContextMenu?: (e: React.MouseEvent) => void;
|
onShareContextMenu?: (e: React.MouseEvent) => void;
|
||||||
onToggleVideo?: () => void;
|
onToggleVideo?: () => void;
|
||||||
onToggleDeafen: () => void;
|
onToggleDeafen: () => void;
|
||||||
onToggleNoiseSuppression?: () => void;
|
|
||||||
onHangup: () => void;
|
onHangup: () => void;
|
||||||
onOpenParticipants?: () => void;
|
onOpenParticipants?: () => void;
|
||||||
/** Toggle the in-call soundboard popover. Active = panel currently open. */
|
/** Toggle the in-call soundboard popover. Active = panel currently open. */
|
||||||
@@ -48,13 +45,11 @@ export function CallControls({
|
|||||||
sharing,
|
sharing,
|
||||||
video,
|
video,
|
||||||
deafened,
|
deafened,
|
||||||
noiseSuppression,
|
|
||||||
onToggleMute,
|
onToggleMute,
|
||||||
onToggleShare,
|
onToggleShare,
|
||||||
onShareContextMenu,
|
onShareContextMenu,
|
||||||
onToggleVideo,
|
onToggleVideo,
|
||||||
onToggleDeafen,
|
onToggleDeafen,
|
||||||
onToggleNoiseSuppression,
|
|
||||||
onHangup,
|
onHangup,
|
||||||
onOpenParticipants,
|
onOpenParticipants,
|
||||||
onToggleSoundboard,
|
onToggleSoundboard,
|
||||||
@@ -134,26 +129,6 @@ export function CallControls({
|
|||||||
<MonitorShareIcon className="h-5 w-5" />
|
<MonitorShareIcon className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</CallButton>
|
</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 && (
|
{onToggleSoundboard && (
|
||||||
<CallButton
|
<CallButton
|
||||||
label={t('app:soundboard.toggle', { defaultValue: 'Soundboard' })}
|
label={t('app:soundboard.toggle', { defaultValue: 'Soundboard' })}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { type CallMode, useCall } from '../context/CallContext';
|
import { type CallMode, useCall } from '../context/CallContext';
|
||||||
import {
|
|
||||||
getAudioSettings,
|
|
||||||
subscribeAudioSettings,
|
|
||||||
updateAudioSettings,
|
|
||||||
} from '../lib/audioSettings';
|
|
||||||
import {
|
import {
|
||||||
getPttSettings,
|
getPttSettings,
|
||||||
type PttSettings,
|
type PttSettings,
|
||||||
@@ -80,7 +75,6 @@ export function InCallPanel({ conversation }: Props) {
|
|||||||
hangup,
|
hangup,
|
||||||
setCallMode,
|
setCallMode,
|
||||||
setFocusedId,
|
setFocusedId,
|
||||||
setAudioInputDevice,
|
|
||||||
micError,
|
micError,
|
||||||
clearMicError,
|
clearMicError,
|
||||||
retryMic,
|
retryMic,
|
||||||
@@ -94,10 +88,6 @@ export function InCallPanel({ conversation }: Props) {
|
|||||||
const [volumeMenu, setVolumeMenu] = useState<
|
const [volumeMenu, setVolumeMenu] = useState<
|
||||||
{ userId: string; displayName: string; x: number; y: number } | null
|
{ userId: string; displayName: string; x: number; y: number } | null
|
||||||
>(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"
|
// Active-speaker auto-focus uses "who most recently started speaking"
|
||||||
// rather than "exactly one speaker" — matches Discord more closely and
|
// rather than "exactly one speaker" — matches Discord more closely and
|
||||||
// handles the case where two people talk briefly without the focus
|
// handles the case where two people talk briefly without the focus
|
||||||
@@ -180,7 +170,6 @@ export function InCallPanel({ conversation }: Props) {
|
|||||||
sharing={isScreenSharing}
|
sharing={isScreenSharing}
|
||||||
video={isCameraEnabled}
|
video={isCameraEnabled}
|
||||||
deafened={isDeafened}
|
deafened={isDeafened}
|
||||||
noiseSuppression={noiseSuppression}
|
|
||||||
onToggleMute={toggleMute}
|
onToggleMute={toggleMute}
|
||||||
// 1-click share uses last-saved preset + displaySurface. Right-click
|
// 1-click share uses last-saved preset + displaySurface. Right-click
|
||||||
// opens the quality picker for users who want to change settings
|
// opens the quality picker for users who want to change settings
|
||||||
@@ -198,14 +187,6 @@ export function InCallPanel({ conversation }: Props) {
|
|||||||
}}
|
}}
|
||||||
onToggleVideo={() => void toggleCamera()}
|
onToggleVideo={() => void toggleCamera()}
|
||||||
onToggleDeafen={toggleDeafen}
|
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)}
|
onOpenParticipants={() => setParticipantsOpen((v) => !v)}
|
||||||
participantsOpen={participantsOpen}
|
participantsOpen={participantsOpen}
|
||||||
onToggleSoundboard={() => setSoundboardOpen((v) => !v)}
|
onToggleSoundboard={() => setSoundboardOpen((v) => !v)}
|
||||||
|
|||||||
@@ -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 ------------------------------------------------------
|
// --- Push-to-talk ------------------------------------------------------
|
||||||
// While PTT is active + we're in a connected call, the mic is held off
|
// 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
|
// except while the configured key is pressed. Under Tauri we also register
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ const DEFAULTS: AudioSettings = {
|
|||||||
inputDeviceId: null,
|
inputDeviceId: null,
|
||||||
outputDeviceId: null,
|
outputDeviceId: null,
|
||||||
voiceThreshold: 0.03,
|
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,
|
videoBackgroundBlur: false,
|
||||||
ringtoneVolume: 0.9,
|
ringtoneVolume: 0.9,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user