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:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import {
|
||||
@@ -6,26 +6,52 @@ import {
|
||||
setParticipantVolume,
|
||||
subscribeParticipantVolumes,
|
||||
} from '../lib/participantVolumes';
|
||||
import {
|
||||
AtIcon,
|
||||
MicIcon,
|
||||
MicOffIcon,
|
||||
PinIcon,
|
||||
PinOffIcon,
|
||||
} from './icons';
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
x: number;
|
||||
y: number;
|
||||
/** Discord-style pin toggle row at the top of the menu. When undefined,
|
||||
* the row is hidden — used for tiles that don't make sense to pin. */
|
||||
pinned?: boolean;
|
||||
onTogglePin?: () => void;
|
||||
/** Hide the volume slider — for self-tiles where the slider would adjust
|
||||
* the local user's own playback gain (which we don't expose). Defaults to
|
||||
* true so existing call sites keep their behaviour. */
|
||||
renderVolume?: boolean;
|
||||
/** Discord-parity: open this user's profile card from the menu. When
|
||||
* undefined the row is hidden (e.g. for self-tiles where it'd just open
|
||||
* your own profile). */
|
||||
onShowProfile?: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const MENU_W = 240;
|
||||
const MENU_H = 96;
|
||||
const MENU_H = 138;
|
||||
|
||||
export function ParticipantVolumeMenu({
|
||||
userId,
|
||||
displayName,
|
||||
x,
|
||||
y,
|
||||
pinned,
|
||||
onTogglePin,
|
||||
renderVolume = true,
|
||||
onShowProfile,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const [volume, setVolume] = useState<number>(() => getParticipantVolume(userId));
|
||||
// Remembers the volume before "Für mich stummschalten" so unmute restores
|
||||
// it instead of snapping back to 100%.
|
||||
const preMuteRef = useRef<number | null>(volume > 0 ? volume : null);
|
||||
|
||||
// Re-sync from store in case another menu instance changed the same user.
|
||||
useEffect(() => subscribeParticipantVolumes(() => {
|
||||
@@ -63,35 +89,99 @@ export function ParticipantVolumeMenu({
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-2 text-xs">
|
||||
<span className="truncate font-semibold text-fg">{displayName}</span>
|
||||
<span
|
||||
{renderVolume && (
|
||||
<span
|
||||
className={
|
||||
'tabular-nums ' + (volume > 1 ? 'text-amber-500' : 'text-fg-muted')
|
||||
}
|
||||
>
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{onShowProfile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onShowProfile();
|
||||
onClose();
|
||||
}}
|
||||
className="mb-1.5 flex w-full cursor-pointer items-center gap-2 rounded-md border border-line bg-surface-3 px-2.5 py-1.5 text-xs font-medium text-fg transition hover:border-accent hover:bg-accent/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<AtIcon className="h-3.5 w-3.5" />
|
||||
<span>Profil anzeigen</span>
|
||||
</button>
|
||||
)}
|
||||
{renderVolume && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Discord-parity "Mute for me": toggle local playback gain.
|
||||
// Storing the pre-mute level lets unmute restore the user's
|
||||
// chosen volume (e.g. 130% → 0 → 130%) instead of snapping to 100.
|
||||
if (volume === 0) {
|
||||
const restored = preMuteRef.current ?? 1;
|
||||
setVolume(restored);
|
||||
setParticipantVolume(userId, restored);
|
||||
} else {
|
||||
preMuteRef.current = volume;
|
||||
setVolume(0);
|
||||
setParticipantVolume(userId, 0);
|
||||
}
|
||||
}}
|
||||
className={
|
||||
'tabular-nums ' + (volume > 1 ? 'text-amber-500' : 'text-fg-muted')
|
||||
'mb-1.5 flex w-full cursor-pointer items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
|
||||
(volume === 0
|
||||
? 'border-rose-500/40 bg-rose-500/15 text-rose-600 hover:bg-rose-500/20 dark:text-rose-300'
|
||||
: 'border-line bg-surface-3 text-fg hover:border-accent hover:bg-accent/10')
|
||||
}
|
||||
>
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
{/* Up to 200% via WebAudio gain. Values above 100% can clip on loud
|
||||
mics — the amber count-up hints at that without a verbose warning. */}
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.01}
|
||||
value={volume}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
setVolume(v);
|
||||
setParticipantVolume(userId, v);
|
||||
}}
|
||||
aria-label={'Lautstärke ' + displayName}
|
||||
className="w-full accent-accent"
|
||||
/>
|
||||
<div className="mt-1 flex justify-between text-[10px] text-fg-muted">
|
||||
<span>0%</span>
|
||||
<span className="tabular-nums">100%</span>
|
||||
<span>200%</span>
|
||||
</div>
|
||||
{volume === 0 ? (
|
||||
<MicIcon className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<MicOffIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{volume === 0 ? 'Lokalen Ton aktivieren' : 'Für mich stummschalten'}</span>
|
||||
</button>
|
||||
)}
|
||||
{onTogglePin && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onTogglePin();
|
||||
onClose();
|
||||
}}
|
||||
className="mb-2 flex w-full cursor-pointer items-center gap-2 rounded-md border border-line bg-surface-3 px-2.5 py-1.5 text-xs font-medium text-fg transition hover:border-accent hover:bg-accent/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
{pinned ? <PinOffIcon className="h-3.5 w-3.5" /> : <PinIcon className="h-3.5 w-3.5" />}
|
||||
<span>{pinned ? 'Anpinnen aufheben' : 'Anpinnen'}</span>
|
||||
</button>
|
||||
)}
|
||||
{renderVolume && (
|
||||
<>
|
||||
{/* Up to 200% via WebAudio gain. Values above 100% can clip on loud
|
||||
mics — the amber count-up hints at that without a verbose warning. */}
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.01}
|
||||
value={volume}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
setVolume(v);
|
||||
setParticipantVolume(userId, v);
|
||||
}}
|
||||
aria-label={'Lautstärke ' + displayName}
|
||||
className="w-full accent-accent"
|
||||
/>
|
||||
<div className="mt-1 flex justify-between text-[10px] text-fg-muted">
|
||||
<span>0%</span>
|
||||
<span className="tabular-nums">100%</span>
|
||||
<span>200%</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user