fix(voice): keep saved linux audio apps in the source picker (#3150)

This commit is contained in:
Hampus
2026-10-03 02:14:06 +02:00
committed by GitHub
parent cf9752db4f
commit eb0e8366bc
2 changed files with 32 additions and 14 deletions
@@ -15,8 +15,10 @@ import {
type LinuxAudioSourceFilterOptions,
type LinuxAudioSourceItem,
linuxAudioSourceItemKey,
linuxAudioSourcePatternsEqual,
mapLinuxAudioNodeToItems,
uniqueLinuxAudioSourceItems,
withSelectedLinuxAudioSources,
} from '@app/features/voice/utils/LinuxAudioSourceRules';
import {
formatScreenShareAudioSummary,
@@ -78,15 +80,8 @@ const EMPTY_SNAPSHOT: AudioSourceSnapshot = {
error: null,
};
function nodesEqual(a: VirtmicNode, b: VirtmicNode): boolean {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => a[key] === b[key]);
}
function isItemSelected(value: VirtmicNode, sources: Array<VirtmicNode>): boolean {
return sources.some((source) => nodesEqual(source, value));
return sources.some((source) => linuxAudioSourcePatternsEqual(source, value));
}
async function fetchAudioSources(options: LinuxAudioSourceFilterOptions): Promise<AudioSourceSnapshot> {
@@ -188,7 +183,7 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
(item: LinuxAudioSourceItem) => {
const isSelected = isItemSelected(item.value, includeSources);
const nextSources = isSelected
? includeSources.filter((source) => !nodesEqual(source, item.value))
? includeSources.filter((source) => !linuxAudioSourcePatternsEqual(source, item.value))
: [...includeSources, item.value];
VoiceSettingsCommands.update({
screenShareAudioSourceMode: nextSources.length > 0 ? 'specific' : 'system',
@@ -203,7 +198,7 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
(item: LinuxAudioSourceItem) => {
const isSelected = isItemSelected(item.value, excludeSources);
const nextSources = isSelected
? excludeSources.filter((source) => !nodesEqual(source, item.value))
? excludeSources.filter((source) => !linuxAudioSourcePatternsEqual(source, item.value))
: [...excludeSources, item.value];
VoiceSettingsCommands.update({
screenShareAudioExcludeSources: nextSources,
@@ -230,6 +225,8 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
usesDeviceMicrophone,
});
const showsWideSourceLists = !offersWindowScope || resolvedScope === 'system';
const includeItems = withSelectedLinuxAudioSources(snapshot.items, includeSources);
const excludeItems = withSelectedLinuxAudioSources(snapshot.items, excludeSources);
const wideSourceIsSelected = isDeviceShare ? usesDeviceMicrophone : sourceMode === 'system';
if (!snapshot.available && !snapshot.loading) {
return null;
@@ -269,12 +266,12 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
</MenuItemRadio>
)}
</MenuGroup>
{showsWideSourceLists && snapshot.items.length > 0 && (
{showsWideSourceLists && includeItems.length > 0 && (
<MenuGroup data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.menu-group--2">
<MenuGroupLabel data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.group-label.include-apps">
{i18n._(INCLUDE_APPS_DESCRIPTOR)}
</MenuGroupLabel>
{snapshot.items.map((item) => (
{includeItems.map((item) => (
<CheckboxItem
key={linuxAudioSourceItemKey(item)}
checked={isItemSelected(item.value, includeSources)}
@@ -286,12 +283,12 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
))}
</MenuGroup>
)}
{!isDeviceShare && showsWideSourceLists && sourceMode === 'system' && snapshot.items.length > 0 && (
{!isDeviceShare && showsWideSourceLists && sourceMode === 'system' && excludeItems.length > 0 && (
<MenuGroup data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.menu-group--3">
<MenuGroupLabel data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.group-label.exclude-from-system">
{i18n._(EXCLUDE_FROM_SYSTEM_DESCRIPTOR)}
</MenuGroupLabel>
{snapshot.items.map((item) => (
{excludeItems.map((item) => (
<CheckboxItem
key={`exclude-${linuxAudioSourceItemKey(item)}`}
checked={isItemSelected(item.value, excludeSources)}
@@ -135,6 +135,27 @@ export function mapLinuxAudioNodeToItems(
return items;
}
export function linuxAudioSourcePatternsEqual(a: VirtmicNode, b: VirtmicNode): boolean {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => a[key] === b[key]);
}
export function withSelectedLinuxAudioSources(
items: ReadonlyArray<LinuxAudioSourceItem>,
selected: ReadonlyArray<VirtmicNode>,
): Array<LinuxAudioSourceItem> {
const missing = selected.filter((source) => !items.some((item) => linuxAudioSourcePatternsEqual(item.value, source)));
return [
...items,
...missing.map((source) => ({
name: getLinuxAudioSourceDisplayName(source) ?? JSON.stringify(source),
value: source,
})),
];
}
export function uniqueLinuxAudioSourceItems(items: Array<LinuxAudioSourceItem>): Array<LinuxAudioSourceItem> {
const seen = new Set<string>();
const result: Array<LinuxAudioSourceItem> = [];