feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { playNotificationTone } from '../lib/notificationSound';
|
||||
import {
|
||||
clearCustomNotificationSound,
|
||||
getCustomNotificationSoundMeta,
|
||||
MAX_NOTIFICATION_SOUND_BYTES,
|
||||
type NotificationSoundEntry,
|
||||
setCustomNotificationSound,
|
||||
subscribeNotificationSoundChanges,
|
||||
} from '../lib/notificationSoundStorage';
|
||||
import { SpinnerIcon, TrashIcon } from './icons';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const BYTES_PER_MB = 1024 * 1024;
|
||||
|
||||
// UI for the custom new-message notification chime. Single file slot.
|
||||
// Preview button calls playNotificationTone() directly so the user
|
||||
// hears exactly what a real incoming message will sound like (same
|
||||
// AudioContext, same volume curve).
|
||||
export function NotificationSoundSettings({ disabled = false }: Props) {
|
||||
const { t } = useTranslation(['app']);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [current, setCurrent] = useState<NotificationSoundEntry | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const meta = await getCustomNotificationSoundMeta();
|
||||
setCurrent(meta);
|
||||
} catch (err: unknown) {
|
||||
console.error('getCustomNotificationSoundMeta failed', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
return subscribeNotificationSoundChanges(() => {
|
||||
void refresh();
|
||||
});
|
||||
}, [refresh]);
|
||||
|
||||
async function handleFile(file: File): Promise<void> {
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
await setCustomNotificationSound(file);
|
||||
// refresh fires via subscribeNotificationSoundChanges
|
||||
} catch (err: unknown) {
|
||||
const code = err instanceof Error ? err.message : 'upload_failed';
|
||||
if (code === 'sound_too_large') {
|
||||
setError(
|
||||
t('app:settings.notif_sound_error_too_large', {
|
||||
defaultValue: 'Datei zu groß (max {{max}} MB).',
|
||||
max: MAX_NOTIFICATION_SOUND_BYTES / BYTES_PER_MB,
|
||||
}),
|
||||
);
|
||||
} else if (code === 'sound_not_audio') {
|
||||
setError(
|
||||
t('app:settings.notif_sound_error_not_audio', {
|
||||
defaultValue: 'Nur Audio-Dateien werden unterstützt.',
|
||||
}),
|
||||
);
|
||||
} else if (code === 'empty_file') {
|
||||
setError(
|
||||
t('app:settings.notif_sound_error_empty', {
|
||||
defaultValue: 'Leere Datei.',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setError(
|
||||
t('app:settings.notif_sound_error_generic', {
|
||||
defaultValue: 'Ton konnte nicht gespeichert werden.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (inputRef.current) inputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReset(): Promise<void> {
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
await clearCustomNotificationSound();
|
||||
// refresh fires via subscribeNotificationSoundChanges
|
||||
} catch (err: unknown) {
|
||||
console.error('clearCustomNotificationSound failed', err);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreview(): void {
|
||||
// Same code path as a real notification — user hears exactly what
|
||||
// peers triggering a message will cause.
|
||||
playNotificationTone();
|
||||
}
|
||||
|
||||
const hasCustom = current !== null;
|
||||
const sizeMb = current ? (current.size / BYTES_PER_MB).toFixed(2) : null;
|
||||
const interactionsDisabled = disabled || busy;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-fg">
|
||||
{t('app:settings.notif_sound_title', { defaultValue: 'Benachrichtigungston' })}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-fg-muted">
|
||||
{hasCustom && current
|
||||
? t('app:settings.notif_sound_custom_active', {
|
||||
defaultValue: '{{name}} · {{size}} MB',
|
||||
name: current.filename,
|
||||
size: sizeMb,
|
||||
})
|
||||
: t('app:settings.notif_sound_default_active', {
|
||||
defaultValue: 'Standard (Zwei-Ton-Chime)',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePreview}
|
||||
disabled={interactionsDisabled}
|
||||
className="cursor-pointer rounded-lg border border-line bg-surface-3 px-3 py-1.5 text-xs font-semibold text-fg transition hover:brightness-95 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{t('app:settings.notif_sound_preview', { defaultValue: 'Probe hören' })}
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) void handleFile(f);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
disabled={interactionsDisabled}
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{busy && <SpinnerIcon className="h-3.5 w-3.5" />}
|
||||
<span>
|
||||
{hasCustom
|
||||
? t('app:settings.notif_sound_replace', { defaultValue: 'Ersetzen' })
|
||||
: t('app:settings.notif_sound_upload', { defaultValue: 'Hochladen' })}
|
||||
</span>
|
||||
</button>
|
||||
{hasCustom && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleReset()}
|
||||
disabled={interactionsDisabled}
|
||||
aria-label={t('app:settings.notif_sound_reset', { defaultValue: 'Zurücksetzen' })}
|
||||
title={t('app:settings.notif_sound_reset', { defaultValue: 'Zurücksetzen' })}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-rose-500/40 bg-rose-500/10 text-rose-600 transition hover:bg-rose-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:text-rose-300"
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="rounded-md border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-xs text-rose-600 dark:text-rose-300">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-[11px] text-fg-muted">
|
||||
{t('app:settings.notif_sound_hint', {
|
||||
defaultValue:
|
||||
'MP3, WAV, OGG oder M4A bis 1 MB. Spielt bei neuen Nachrichten in Chats die du nicht aktiv ansiehst.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user