mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(voice): stop the in call speaker slider clamping a boosted output volume (#1785)
This commit is contained in:
1 parent
17eab43245
commit
ef6536644c
5 files changed
+48
-11
No files matched your search
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@app/features/voice/components/CallVolumeState';
|
} from '@app/features/voice/components/CallVolumeState';
|
||||||
import {MediaVerticalVolumeControl} from '@app/features/voice/components/media_player/components/MediaVerticalVolumeControl';
|
import {MediaVerticalVolumeControl} from '@app/features/voice/components/media_player/components/MediaVerticalVolumeControl';
|
||||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||||
|
import {VOICE_VOLUME_MAX_SLIDER_VOLUME} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {useCallback, useEffect, useRef} from 'react';
|
import {useCallback, useEffect, useRef} from 'react';
|
||||||
@@ -30,7 +31,9 @@ export const CallVolumeControl: React.FC<CallVolumeControlProps> = observer(func
|
|||||||
lastNonZeroVolumeRef.current = resolveLastNonZeroCallVolume(outputVolume, lastNonZeroVolumeRef.current);
|
lastNonZeroVolumeRef.current = resolveLastNonZeroCallVolume(outputVolume, lastNonZeroVolumeRef.current);
|
||||||
}, [outputVolume]);
|
}, [outputVolume]);
|
||||||
const handleVolumeChange = useCallback((volume: number) => {
|
const handleVolumeChange = useCallback((volume: number) => {
|
||||||
VoiceSettingsCommands.update({outputVolume: sliderVolumeToCallVolumePercent(volume)});
|
const nextPercent = sliderVolumeToCallVolumePercent(volume);
|
||||||
|
if (nextPercent === VoiceSettings.outputVolume) return;
|
||||||
|
VoiceSettingsCommands.update({outputVolume: nextPercent});
|
||||||
}, []);
|
}, []);
|
||||||
const handleToggleMute = useCallback(() => {
|
const handleToggleMute = useCallback(() => {
|
||||||
VoiceSettingsCommands.update({
|
VoiceSettingsCommands.update({
|
||||||
@@ -41,6 +44,7 @@ export const CallVolumeControl: React.FC<CallVolumeControlProps> = observer(func
|
|||||||
<MediaVerticalVolumeControl
|
<MediaVerticalVolumeControl
|
||||||
volume={callVolumePercentToSliderVolume(outputVolume)}
|
volume={callVolumePercentToSliderVolume(outputVolume)}
|
||||||
isMuted={outputVolume === 0}
|
isMuted={outputVolume === 0}
|
||||||
|
maxVolume={VOICE_VOLUME_MAX_SLIDER_VOLUME}
|
||||||
onVolumeChange={handleVolumeChange}
|
onVolumeChange={handleVolumeChange}
|
||||||
onToggleMute={handleToggleMute}
|
onToggleMute={handleToggleMute}
|
||||||
iconSize={iconSize}
|
iconSize={iconSize}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ describe('callVolumePercentToSliderVolume', () => {
|
|||||||
it('clamps negative percent to zero', () => {
|
it('clamps negative percent to zero', () => {
|
||||||
expect(callVolumePercentToSliderVolume(-10)).toBe(0);
|
expect(callVolumePercentToSliderVolume(-10)).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reaches the boosted ceiling and clamps past it', () => {
|
||||||
|
expect(callVolumePercentToSliderVolume(200)).toBe(2);
|
||||||
|
expect(callVolumePercentToSliderVolume(250)).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sliderVolumeToCallVolumePercent', () => {
|
describe('sliderVolumeToCallVolumePercent', () => {
|
||||||
@@ -34,7 +39,12 @@ describe('sliderVolumeToCallVolumePercent', () => {
|
|||||||
|
|
||||||
it('clamps out-of-range slider values', () => {
|
it('clamps out-of-range slider values', () => {
|
||||||
expect(sliderVolumeToCallVolumePercent(-0.5)).toBe(0);
|
expect(sliderVolumeToCallVolumePercent(-0.5)).toBe(0);
|
||||||
expect(sliderVolumeToCallVolumePercent(2)).toBe(100);
|
expect(sliderVolumeToCallVolumePercent(2.5)).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps the boosted range above unity', () => {
|
||||||
|
expect(sliderVolumeToCallVolumePercent(1.5)).toBe(150);
|
||||||
|
expect(sliderVolumeToCallVolumePercent(2)).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the default for non-finite input', () => {
|
it('falls back to the default for non-finite input', () => {
|
||||||
@@ -70,3 +80,18 @@ describe('resolveCallVolumeMuteToggle', () => {
|
|||||||
expect(resolveCallVolumeMuteToggle(0, 0)).toBe(CALL_VOLUME_DEFAULT_PERCENT);
|
expect(resolveCallVolumeMuteToggle(0, 0)).toBe(CALL_VOLUME_DEFAULT_PERCENT);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('call volume round trip', () => {
|
||||||
|
it('preserves a stored boosted volume through the slider', () => {
|
||||||
|
expect(sliderVolumeToCallVolumePercent(callVolumePercentToSliderVolume(200))).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('steps a boosted volume down by one keyboard step instead of collapsing to unity', () => {
|
||||||
|
expect(sliderVolumeToCallVolumePercent(Math.max(0, callVolumePercentToSliderVolume(200) - 0.1))).toBe(190);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores a boosted volume after a mute toggle', () => {
|
||||||
|
expect(resolveCallVolumeMuteToggle(200, 200)).toBe(0);
|
||||||
|
expect(resolveCallVolumeMuteToggle(0, resolveLastNonZeroCallVolume(0, 200))).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {VOICE_VOLUME_MAX_PERCENT} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||||
|
|
||||||
export const CALL_VOLUME_DEFAULT_PERCENT = 100;
|
export const CALL_VOLUME_DEFAULT_PERCENT = 100;
|
||||||
export const CALL_VOLUME_SLIDER_MAX_PERCENT = 100;
|
export const CALL_VOLUME_UNITY_PERCENT = 100;
|
||||||
|
export const CALL_VOLUME_SLIDER_MAX_PERCENT = VOICE_VOLUME_MAX_PERCENT;
|
||||||
|
|
||||||
export function callVolumePercentToSliderVolume(percent: number): number {
|
export function callVolumePercentToSliderVolume(percent: number): number {
|
||||||
if (!Number.isFinite(percent)) {
|
if (!Number.isFinite(percent)) {
|
||||||
return CALL_VOLUME_DEFAULT_PERCENT / CALL_VOLUME_SLIDER_MAX_PERCENT;
|
return CALL_VOLUME_DEFAULT_PERCENT / CALL_VOLUME_UNITY_PERCENT;
|
||||||
}
|
}
|
||||||
return Math.max(0, percent / CALL_VOLUME_SLIDER_MAX_PERCENT);
|
const clamped = Math.max(0, Math.min(CALL_VOLUME_SLIDER_MAX_PERCENT, percent));
|
||||||
|
return clamped / CALL_VOLUME_UNITY_PERCENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sliderVolumeToCallVolumePercent(volume: number): number {
|
export function sliderVolumeToCallVolumePercent(volume: number): number {
|
||||||
if (!Number.isFinite(volume)) {
|
if (!Number.isFinite(volume)) {
|
||||||
return CALL_VOLUME_DEFAULT_PERCENT;
|
return CALL_VOLUME_DEFAULT_PERCENT;
|
||||||
}
|
}
|
||||||
const percent = Math.round(volume * CALL_VOLUME_SLIDER_MAX_PERCENT);
|
const percent = Math.round(volume * CALL_VOLUME_UNITY_PERCENT);
|
||||||
return Math.max(0, Math.min(CALL_VOLUME_SLIDER_MAX_PERCENT, percent));
|
return Math.max(0, Math.min(CALL_VOLUME_SLIDER_MAX_PERCENT, percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import VoiceCallLayout from '@app/features/voice/state/VoiceCallLayout';
|
|||||||
import {hasValidRoomForVoiceCallContext} from '@app/features/voice/utils/VoiceCallContext';
|
import {hasValidRoomForVoiceCallContext} from '@app/features/voice/utils/VoiceCallContext';
|
||||||
import {VOICE_CALL_DESCRIPTOR} from '@app/features/voice/utils/VoiceMessageDescriptors';
|
import {VOICE_CALL_DESCRIPTOR} from '@app/features/voice/utils/VoiceMessageDescriptors';
|
||||||
import {parseVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity';
|
import {parseVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity';
|
||||||
|
import {VOICE_VOLUME_MAX_SLIDER_VOLUME} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||||
import {msg, plural} from '@lingui/core/macro';
|
import {msg, plural} from '@lingui/core/macro';
|
||||||
import {useLingui} from '@lingui/react/macro';
|
import {useLingui} from '@lingui/react/macro';
|
||||||
@@ -620,6 +621,7 @@ const VoiceCallViewInner = observer(
|
|||||||
<MediaVerticalVolumeControl
|
<MediaVerticalVolumeControl
|
||||||
volume={focusedStreamVolume / 100}
|
volume={focusedStreamVolume / 100}
|
||||||
isMuted={isFocusedStreamMuted}
|
isMuted={isFocusedStreamMuted}
|
||||||
|
maxVolume={VOICE_VOLUME_MAX_SLIDER_VOLUME}
|
||||||
onVolumeChange={handleFocusedStreamVolumeChange}
|
onVolumeChange={handleFocusedStreamVolumeChange}
|
||||||
onToggleMute={handleFocusedStreamToggleMute}
|
onToggleMute={handleFocusedStreamToggleMute}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
|
|||||||
+7
-5
@@ -48,6 +48,7 @@ interface MediaVerticalVolumeControlProps {
|
|||||||
iconSize?: number;
|
iconSize?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
position?: 'above' | 'below';
|
position?: 'above' | 'below';
|
||||||
|
maxVolume?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVolumeIcon(volume: number, isMuted: boolean) {
|
function getVolumeIcon(volume: number, isMuted: boolean) {
|
||||||
@@ -73,6 +74,7 @@ export function MediaVerticalVolumeControl({
|
|||||||
iconSize = 18,
|
iconSize = 18,
|
||||||
className,
|
className,
|
||||||
position = 'above',
|
position = 'above',
|
||||||
|
maxVolume = 1,
|
||||||
}: MediaVerticalVolumeControlProps) {
|
}: MediaVerticalVolumeControlProps) {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
@@ -178,9 +180,9 @@ export function MediaVerticalVolumeControl({
|
|||||||
}, [isDragging, scheduleClose]);
|
}, [isDragging, scheduleClose]);
|
||||||
const handleSliderValueChange = useCallback(
|
const handleSliderValueChange = useCallback(
|
||||||
(nextValue: number) => {
|
(nextValue: number) => {
|
||||||
onVolumeChange(Math.max(0, Math.min(1, nextValue / 100)));
|
onVolumeChange(Math.max(0, Math.min(maxVolume, nextValue / 100)));
|
||||||
},
|
},
|
||||||
[onVolumeChange],
|
[maxVolume, onVolumeChange],
|
||||||
);
|
);
|
||||||
const handleSliderInteractionChange = useCallback(
|
const handleSliderInteractionChange = useCallback(
|
||||||
(nextIsDragging: boolean) => {
|
(nextIsDragging: boolean) => {
|
||||||
@@ -209,7 +211,7 @@ export function MediaVerticalVolumeControl({
|
|||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
newVolume = Math.min(1, volume + step);
|
newVolume = Math.min(maxVolume, volume + step);
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
@@ -228,7 +230,7 @@ export function MediaVerticalVolumeControl({
|
|||||||
}
|
}
|
||||||
onVolumeChange(newVolume);
|
onVolumeChange(newVolume);
|
||||||
},
|
},
|
||||||
[volume, onVolumeChange, onToggleMute],
|
[maxVolume, volume, onVolumeChange, onToggleMute],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -264,7 +266,7 @@ export function MediaVerticalVolumeControl({
|
|||||||
defaultValue={sliderValue}
|
defaultValue={sliderValue}
|
||||||
factoryDefaultValue={100}
|
factoryDefaultValue={100}
|
||||||
minValue={0}
|
minValue={0}
|
||||||
maxValue={100}
|
maxValue={maxVolume * 100}
|
||||||
step={1}
|
step={1}
|
||||||
value={sliderValue}
|
value={sliderValue}
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
|
|||||||
Reference in new issue
Block a user