refactor(app): name mute and unread state accessors by intent (#1859)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:32:55 +02:00
1 parent 52781cfa2d
commit a2099fb0e2
33 files changed
+173 -490

No files matched your search

@@ -1,192 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
vi.mock('@app/features/app/state/RuntimeConfig', () => ({
default: {staticCdnEndpoint: 'https://cdn.test'},
}));
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
default: {
getDeepFilterNoiseSuppression: () => true,
getDeepFilterNoiseSuppressionLevel: () => 80,
},
}));
const deepFilterHarness = vi.hoisted(() => ({
instances: [] as Array<{
audioContext: AudioContext | null;
processedTrack: MediaStreamTrack | null;
sampleRateAtInit: number | null;
}>,
}));
vi.mock('deepfilternet3-noise-filter', () => ({
DeepFilterNoiseFilterProcessor: class {
audioContext: AudioContext | null = null;
processedTrack: MediaStreamTrack | null = null;
sampleRateAtInit: number | null = null;
constructor() {
deepFilterHarness.instances.push(this);
}
async init(): Promise<void> {
this.sampleRateAtInit = this.audioContext?.sampleRate ?? null;
this.processedTrack = {} as MediaStreamTrack;
}
async destroy(): Promise<void> {}
},
}));
const MODEL_SAMPLE_RATE = 48000;
interface ContextHarness {
requestedRates: Array<number | undefined>;
closedRates: Array<number>;
}
function installAudioGraph(config: {hardwareSampleRate: number; canBridgeSampleRates: boolean}): ContextHarness {
const harness: ContextHarness = {requestedRates: [], closedRates: []};
class FakeAudioContext {
state: AudioContextState = 'running';
sampleRate: number;
constructor(options?: AudioContextOptions) {
harness.requestedRates.push(options?.sampleRate);
this.sampleRate = options?.sampleRate ?? config.hardwareSampleRate;
}
createMediaStreamSource(): {disconnect: () => void} {
if (!config.canBridgeSampleRates && this.sampleRate !== config.hardwareSampleRate) {
throw new DOMException(
'Connecting AudioNodes from AudioContexts with different sample-rate is currently not supported.',
'NotSupportedError',
);
}
return {disconnect: (): void => undefined};
}
async close(): Promise<void> {
harness.closedRates.push(this.sampleRate);
this.state = 'closed';
}
}
vi.stubGlobal('window', {AudioContext: FakeAudioContext});
vi.stubGlobal('MediaStream', class {});
return harness;
}
async function loadModule() {
vi.resetModules();
return await import('@app/features/voice/utils/DeepFilterNoiseProcessor');
}
function createSourceContext(sampleRate: number): AudioContext {
return {sampleRate} as unknown as AudioContext;
}
const feedTrack = {} as MediaStreamTrack;
describe('resolveDeepFilterAudioContext', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('runs the worklet at the model rate when the capture graph already runs there', async () => {
const harness = installAudioGraph({hardwareSampleRate: MODEL_SAMPLE_RATE, canBridgeSampleRates: false});
const {resolveDeepFilterAudioContext} = await loadModule();
const context = resolveDeepFilterAudioContext(createSourceContext(MODEL_SAMPLE_RATE), feedTrack);
expect(context?.sampleRate).toBe(MODEL_SAMPLE_RATE);
expect(harness.requestedRates).toEqual([MODEL_SAMPLE_RATE]);
expect(harness.closedRates).toEqual([]);
});
it('keeps the model rate on browsers that resample across graphs', async () => {
const harness = installAudioGraph({hardwareSampleRate: 44100, canBridgeSampleRates: true});
const {resolveDeepFilterAudioContext} = await loadModule();
const context = resolveDeepFilterAudioContext(createSourceContext(44100), feedTrack);
expect(context?.sampleRate).toBe(MODEL_SAMPLE_RATE);
expect(harness.requestedRates).toEqual([MODEL_SAMPLE_RATE]);
expect(harness.closedRates).toEqual([]);
});
it('falls back to the capture rate when the browser refuses to bridge sample rates', async () => {
const harness = installAudioGraph({hardwareSampleRate: 44100, canBridgeSampleRates: false});
const {resolveDeepFilterAudioContext} = await loadModule();
const context = resolveDeepFilterAudioContext(createSourceContext(44100), feedTrack);
expect(context?.sampleRate).toBe(44100);
expect(harness.requestedRates).toEqual([MODEL_SAMPLE_RATE, 44100]);
expect(harness.closedRates).toEqual([MODEL_SAMPLE_RATE]);
});
it('falls back to the capture rate for a bluetooth headset running well below the model rate', async () => {
installAudioGraph({hardwareSampleRate: 16000, canBridgeSampleRates: false});
const {resolveDeepFilterAudioContext} = await loadModule();
const context = resolveDeepFilterAudioContext(createSourceContext(16000), feedTrack);
expect(context?.sampleRate).toBe(16000);
});
});
function createCaptureContext(sampleRate: number): AudioContext {
const track = {stop: (): void => undefined} as unknown as MediaStreamTrack;
const node = {
connect: (next: unknown): unknown => next,
disconnect: (): void => undefined,
stream: {getAudioTracks: (): Array<MediaStreamTrack> => [track]},
type: '',
frequency: {value: 0},
Q: {value: 0},
threshold: {value: 0},
knee: {value: 0},
ratio: {value: 0},
attack: {value: 0},
release: {value: 0},
};
return {
sampleRate,
createMediaStreamDestination: () => node,
createMediaStreamSource: () => node,
createBiquadFilter: () => node,
createDynamicsCompressor: () => node,
} as unknown as AudioContext;
}
describe('buildDeepFilterAudioChain', () => {
beforeEach(() => {
deepFilterHarness.instances.length = 0;
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('gives the processor its audio context before the processor builds its own graph', async () => {
installAudioGraph({hardwareSampleRate: MODEL_SAMPLE_RATE, canBridgeSampleRates: false});
const {buildDeepFilterAudioChain} = await loadModule();
await buildDeepFilterAudioChain({audioContext: createCaptureContext(MODEL_SAMPLE_RATE)});
expect(deepFilterHarness.instances[0]?.sampleRateAtInit).toBe(MODEL_SAMPLE_RATE);
});
it('gives the processor a capture-rate context when the browser refuses to bridge sample rates', async () => {
installAudioGraph({hardwareSampleRate: 44100, canBridgeSampleRates: false});
const {buildDeepFilterAudioChain} = await loadModule();
await buildDeepFilterAudioChain({audioContext: createCaptureContext(44100)});
expect(deepFilterHarness.instances[0]?.sampleRateAtInit).toBe(44100);
});
});
@@ -1,68 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {beforeEach, describe, expect, it, vi} from 'vitest';
const master = {connect: vi.fn(), disconnect: vi.fn()};
const createdGains: Array<{connect: ReturnType<typeof vi.fn>; disconnect: ReturnType<typeof vi.fn>}> = [];
let captureActive = true;
vi.mock('@app/features/notification/utils/SoundUtils', () => ({
getSoundCaptureAudioContext: () => ({
createMediaElementSource: vi.fn(() => ({connect: vi.fn(), disconnect: vi.fn()})),
createGain: vi.fn(() => {
const gain = {connect: vi.fn(), disconnect: vi.fn(), gain: {value: 1}};
createdGains.push(gain);
return gain;
}),
}),
getSoundCaptureMasterGainNode: () => master,
isSoundCaptureActive: () => captureActive,
onSoundCaptureActivated: () => () => undefined,
}));
const {routeMediaElementForSoundCapture} = await import('@app/features/voice/utils/InAppMediaSoundCapture');
function createElement(): HTMLMediaElement {
const listeners = new Map<string, () => void>();
return {
volume: 1,
muted: false,
addEventListener: (type: string, listener: () => void) => listeners.set(type, listener),
removeEventListener: (type: string) => listeners.delete(type),
listenerCount: () => listeners.size,
} as unknown as HTMLMediaElement & {listenerCount: () => number};
}
describe('in-app media sound capture', () => {
beforeEach(() => {
createdGains.length = 0;
captureActive = true;
});
it('disconnects the element from the master graph when the caller disposes', () => {
const element = createElement();
const dispose = routeMediaElementForSoundCapture(element);
const gain = createdGains.at(-1);
expect(gain?.connect).toHaveBeenCalledWith(master);
dispose();
expect(gain?.disconnect).toHaveBeenCalled();
});
it('removes the volume listener when the caller disposes', () => {
const element = createElement() as HTMLMediaElement & {listenerCount: () => number};
const dispose = routeMediaElementForSoundCapture(element);
expect(element.listenerCount()).toBe(1);
dispose();
expect(element.listenerCount()).toBe(0);
});
it('reuses the existing source node when the same element is routed again', () => {
const element = createElement();
routeMediaElementForSoundCapture(element)();
const gainsAfterFirst = createdGains.length;
const dispose = routeMediaElementForSoundCapture(element);
expect(createdGains.length).toBe(gainsAfterFirst);
expect(createdGains.at(-1)?.connect).toHaveBeenCalledTimes(2);
dispose();
});
});
@@ -1,67 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL,
resolveVoiceProcessing,
type VoiceProcessingSettingsLike,
} from '@app/features/voice/utils/VoiceProcessingProfile';
import {describe, expect, it, vi} from 'vitest';
vi.mock('@app/features/voice/engine/VoiceDevicePermissionState', () => ({
default: {getState: () => ({inputDevices: []})},
}));
vi.mock('@app/features/voice/utils/VoiceDeviceManager', () => ({
resolveEffectiveDeviceId: () => null,
}));
function createSettings(overrides: Partial<VoiceProcessingSettingsLike> = {}): VoiceProcessingSettingsLike {
return {
voiceProcessingMode: 'voice',
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
deepFilterNoiseSuppression: true,
deepFilterNoiseSuppressionLevel: 80,
...overrides,
};
}
describe('resolveVoiceProcessing', () => {
it('keeps automatic gain control on for focused voice at factory defaults', () => {
const resolved = resolveVoiceProcessing(createSettings());
expect(resolved.mode).toBe('voice');
expect(resolved.autoGainControl).toBe(true);
expect(resolved.deepFilter).toBe(true);
expect(resolved.deepFilterNoiseReductionLevel).toBe(FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL);
});
it('honours an explicit opt out of automatic gain control in focused voice', () => {
const resolved = resolveVoiceProcessing(createSettings({autoGainControl: false}));
expect(resolved.autoGainControl).toBe(false);
});
it('keeps automatic gain control on in custom mode while deep filter is enabled', () => {
const resolved = resolveVoiceProcessing(
createSettings({voiceProcessingMode: 'custom', deepFilterNoiseSuppression: true}),
);
expect(resolved.autoGainControl).toBe(true);
expect(resolved.deepFilter).toBe(true);
});
it('still suppresses browser noise suppression when deep filter is enabled in custom mode', () => {
const resolved = resolveVoiceProcessing(
createSettings({voiceProcessingMode: 'custom', noiseSuppression: true, deepFilterNoiseSuppression: true}),
);
expect(resolved.browserNoiseSuppression).toBe(false);
});
it('leaves studio mode as the untouched bypass', () => {
const resolved = resolveVoiceProcessing(createSettings({voiceProcessingMode: 'studio'}));
expect(resolved.autoGainControl).toBe(false);
expect(resolved.echoCancellation).toBe(false);
expect(resolved.browserNoiseSuppression).toBe(false);
expect(resolved.deepFilter).toBe(false);
expect(resolved.contentHint).toBe('music');
});
});
@@ -419,7 +419,7 @@ function handleIncomingTtsMessage(message: Message): void {
}
if (
UserGuildSettings.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) ||
UserGuildSettings.isCategoryMuted(channel.guildId ?? null, channel.id)
UserGuildSettings.isParentCategoryMuted(channel.guildId ?? null, channel.id)
) {
return;
}