mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(voice): allow self-mute while push-to-talk is on (#3451)
This commit is contained in:
1 parent
3657438467
commit
f501d34179
27 files changed
+1025
-79
No files matched your search
@@ -206,6 +206,7 @@ const UserAreaInner = observer(
|
||||
}, [hasVoiceConnection]);
|
||||
const wrapperClassName = styles.userAreaInnerWrapper;
|
||||
const isPushToTalkEffective = Keybind.isPushToTalkEffective();
|
||||
const isSelfMuted = isMuted || muteReason === 'self';
|
||||
const microphoneState = selectUserAreaMicrophoneState({
|
||||
effectiveAudioMuted: isMuted,
|
||||
effectiveAudioDeafened: isDeafened,
|
||||
@@ -214,7 +215,6 @@ const UserAreaInner = observer(
|
||||
isPermissionMuted,
|
||||
muteReason,
|
||||
isPushToTalkEffective,
|
||||
isPushToTalkHeld: Keybind.pushToTalkHeld,
|
||||
isPushToMuteEffective: Keybind.isPushToMuteEffective(),
|
||||
isPushToMuteHeld: Keybind.pushToMuteHeld,
|
||||
});
|
||||
@@ -224,7 +224,8 @@ const UserAreaInner = observer(
|
||||
if (isGuildDeafened) return getVoiceDeafenedByModeratorsStatusLabel(i18n, true);
|
||||
if (isGuildMuted) return i18n._(VOICE_MUTED_BY_MODERATORS_DESCRIPTOR);
|
||||
if (isPermissionMuted || muteReason === 'permission') return i18n._(VOICE_NO_SPEAK_PERMISSION_DESCRIPTOR);
|
||||
if (isPushToTalkEffective) return getPushToTalkHoldLabel(i18n, PUSH_TO_TALK_IS_ON_HOLD_TO_SPEAK_DESCRIPTOR);
|
||||
if (isPushToTalkEffective && !isSelfMuted)
|
||||
return getPushToTalkHoldLabel(i18n, PUSH_TO_TALK_IS_ON_HOLD_TO_SPEAK_DESCRIPTOR);
|
||||
if (effectiveMuted) return i18n._(UNMUTE_MICROPHONE_DESCRIPTOR);
|
||||
return i18n._(MUTE_MICROPHONE_DESCRIPTOR);
|
||||
})();
|
||||
@@ -232,7 +233,8 @@ const UserAreaInner = observer(
|
||||
if (isGuildDeafened) return getVoiceDeafenedByModeratorsStatusLabel(i18n, true);
|
||||
if (isGuildMuted) return i18n._(VOICE_MUTED_BY_MODERATORS_DESCRIPTOR);
|
||||
if (isPermissionMuted || muteReason === 'permission') return i18n._(VOICE_NO_SPEAK_PERMISSION_DESCRIPTOR);
|
||||
if (isPushToTalkEffective) return getPushToTalkHoldLabel(i18n, PUSH_TO_TALK_IS_ON_HOLD_TO_SPEAK_DESCRIPTOR);
|
||||
if (isPushToTalkEffective && !isSelfMuted)
|
||||
return getPushToTalkHoldLabel(i18n, PUSH_TO_TALK_IS_ON_HOLD_TO_SPEAK_DESCRIPTOR);
|
||||
if (effectiveMuted) return i18n._(UNMUTE_MICROPHONE_DESCRIPTOR);
|
||||
return i18n._(MUTE_MICROPHONE_DESCRIPTOR);
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
selectUserAreaMicrophoneState,
|
||||
type UserAreaMicrophoneSignals,
|
||||
} from '@app/features/app/components/layout/UserAreaState';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function signals(overrides: Partial<UserAreaMicrophoneSignals>): UserAreaMicrophoneSignals {
|
||||
return {
|
||||
effectiveAudioMuted: false,
|
||||
effectiveAudioDeafened: false,
|
||||
isGuildMuted: false,
|
||||
isGuildDeafened: false,
|
||||
isPermissionMuted: false,
|
||||
muteReason: null,
|
||||
isPushToTalkEffective: false,
|
||||
isPushToMuteEffective: false,
|
||||
isPushToMuteHeld: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('selectUserAreaMicrophoneState', () => {
|
||||
it('shows the idle push to talk gate as unmuted without locking the toggle', () => {
|
||||
expect(
|
||||
selectUserAreaMicrophoneState(signals({isPushToTalkEffective: true, muteReason: 'voice_push_to_talk'})),
|
||||
).toEqual({effectiveMuted: false, muteToggleLocked: false});
|
||||
});
|
||||
|
||||
it('shows a self-muted push to talk user as muted', () => {
|
||||
expect(
|
||||
selectUserAreaMicrophoneState(
|
||||
signals({isPushToTalkEffective: true, effectiveAudioMuted: true, muteReason: 'self'}),
|
||||
),
|
||||
).toEqual({effectiveMuted: true, muteToggleLocked: false});
|
||||
});
|
||||
|
||||
it('shows a push to talk user muted by the failure latch as muted', () => {
|
||||
expect(selectUserAreaMicrophoneState(signals({isPushToTalkEffective: true, muteReason: 'self'}))).toEqual({
|
||||
effectiveMuted: true,
|
||||
muteToggleLocked: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('lets a self-deafened push to talk user click the toggle', () => {
|
||||
expect(
|
||||
selectUserAreaMicrophoneState(
|
||||
signals({isPushToTalkEffective: true, effectiveAudioMuted: true, effectiveAudioDeafened: true}),
|
||||
),
|
||||
).toEqual({effectiveMuted: true, muteToggleLocked: false});
|
||||
});
|
||||
|
||||
it('locks the toggle for server and permission mutes in push to talk', () => {
|
||||
for (const overrides of [
|
||||
{isGuildMuted: true, muteReason: 'guild' as const},
|
||||
{isGuildDeafened: true},
|
||||
{isPermissionMuted: true, muteReason: 'permission' as const},
|
||||
]) {
|
||||
expect(selectUserAreaMicrophoneState(signals({isPushToTalkEffective: true, ...overrides}))).toEqual({
|
||||
effectiveMuted: true,
|
||||
muteToggleLocked: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps voice activity and push to mute behaviour unchanged', () => {
|
||||
expect(selectUserAreaMicrophoneState(signals({}))).toEqual({effectiveMuted: false, muteToggleLocked: false});
|
||||
expect(selectUserAreaMicrophoneState(signals({effectiveAudioMuted: true, muteReason: 'self'}))).toEqual({
|
||||
effectiveMuted: true,
|
||||
muteToggleLocked: false,
|
||||
});
|
||||
expect(selectUserAreaMicrophoneState(signals({isPushToMuteEffective: true, isPushToMuteHeld: true}))).toEqual({
|
||||
effectiveMuted: true,
|
||||
muteToggleLocked: false,
|
||||
});
|
||||
expect(selectUserAreaMicrophoneState(signals({isGuildMuted: true}))).toEqual({
|
||||
effectiveMuted: true,
|
||||
muteToggleLocked: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ export interface UserAreaMicrophoneSignals {
|
||||
isPermissionMuted: boolean;
|
||||
muteReason: UserAreaMuteReason;
|
||||
isPushToTalkEffective: boolean;
|
||||
isPushToTalkHeld: boolean;
|
||||
isPushToMuteEffective: boolean;
|
||||
isPushToMuteHeld: boolean;
|
||||
}
|
||||
@@ -32,13 +31,13 @@ export function selectUserAreaMicrophoneState(signals: UserAreaMicrophoneSignals
|
||||
if (hardMuted) {
|
||||
return {
|
||||
effectiveMuted: true,
|
||||
muteToggleLocked: muteToggleLocked || signals.isPushToTalkEffective,
|
||||
muteToggleLocked,
|
||||
};
|
||||
}
|
||||
if (signals.isPushToTalkEffective) {
|
||||
return {
|
||||
effectiveMuted: !signals.isPushToTalkHeld,
|
||||
muteToggleLocked: true,
|
||||
effectiveMuted: signals.effectiveAudioMuted || signals.muteReason === 'self',
|
||||
muteToggleLocked,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -521,7 +521,7 @@ class KeybindManager {
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.disposers.push(reactToPushToTalkModeChanges((options) => MediaEngine.handlePushToTalkModeChange(options)));
|
||||
this.disposers.push(reactToPushToTalkModeChanges(() => MediaEngine.handlePushToTalkModeChange()));
|
||||
this.disposers.push(
|
||||
reaction(
|
||||
() => ({entries: this.buildHoldBindingEntries(), routing: this.getGlobalHoldRouting()}),
|
||||
|
||||
@@ -229,7 +229,7 @@ describe('push-to-talk mode reconcile', () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('flips push-to-talk only on settled states and preserves self-mute when it does', () => {
|
||||
it('flips push-to-talk only on settled states and reconciles once per flip', () => {
|
||||
const {Keybind, AppStorage, reactToPushToTalkModeChanges} = modules;
|
||||
const reconcile = vi.fn();
|
||||
const dispose = reactToPushToTalkModeChanges(reconcile);
|
||||
@@ -238,7 +238,7 @@ describe('push-to-talk mode reconcile', () => {
|
||||
emit(status);
|
||||
expect(Keybind.isPushToTalkEffective()).toBe(effective);
|
||||
expect(reconcile).toHaveBeenCalledTimes(1);
|
||||
expect(reconcile).toHaveBeenLastCalledWith({preserveSelfMute: true});
|
||||
expect(reconcile).toHaveBeenLastCalledWith();
|
||||
};
|
||||
expectFlip(portalStatus('declined'), false);
|
||||
expect(AppStorage.getJSON(PORTAL_ASSIGNED_ACTIONS_KEY)).toEqual([]);
|
||||
@@ -251,8 +251,12 @@ describe('push-to-talk mode reconcile', () => {
|
||||
expectFlip(portalStatus('bound'), true);
|
||||
reconcile.mockClear();
|
||||
Keybind.setTransmitMode('voice_activity');
|
||||
expect(reconcile).toHaveBeenLastCalledWith({preserveSelfMute: false});
|
||||
expect(Keybind.isPushToTalkEffective()).toBe(false);
|
||||
expect(reconcile).toHaveBeenCalledTimes(1);
|
||||
expect(reconcile).toHaveBeenLastCalledWith();
|
||||
Keybind.setTransmitMode('voice_push_to_talk');
|
||||
expect(Keybind.isPushToTalkEffective()).toBe(true);
|
||||
expect(reconcile).toHaveBeenCalledTimes(2);
|
||||
dispose();
|
||||
});
|
||||
});
|
||||
@@ -3,14 +3,13 @@
|
||||
import Keybind from '@app/features/input/state/InputKeybind';
|
||||
import {compareStructural, reaction} from 'mobx';
|
||||
|
||||
export function reactToPushToTalkModeChanges(onChange: (options: {preserveSelfMute: boolean}) => void): () => void {
|
||||
export function reactToPushToTalkModeChanges(onChange: () => void): () => void {
|
||||
return reaction(
|
||||
() => ({
|
||||
transmitMode: Keybind.transmitMode,
|
||||
pushToTalk: Keybind.isPushToTalkEffective(),
|
||||
pushToMute: Keybind.isPushToMuteEffective(),
|
||||
}),
|
||||
(current, previous) => onChange({preserveSelfMute: current.transmitMode === previous.transmitMode}),
|
||||
() => onChange(),
|
||||
{equals: compareStructural},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user