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:
byGalax
2026-05-06 23:35:01 +02:00
parent 72d02e385f
commit 825160ee46
504 changed files with 14217 additions and 14366 deletions
@@ -1,6 +1,20 @@
import { useEffect, useRef } from 'react';
import { CrownIcon, HeadphonesOffIcon, LockIcon, MicOffIcon } from './icons';
import {
CrownIcon,
HeadphonesOffIcon,
LockIcon,
MicOffIcon,
PinIcon,
WifiLowIcon,
WifiOffIcon,
} from './icons';
/** Discord-style connection-quality badge state. Matches LiveKit's
* ConnectionQuality enum values so consumers can pass the value through
* without conversion. `unknown` and `excellent`/`good` render nothing — the
* badge only surfaces when the user actually has degraded media. */
export type TileConnectionQuality = 'excellent' | 'good' | 'poor' | 'lost' | 'unknown';
export type AvatarColorKey = 'violet' | 'amber' | 'rose' | 'teal';
@@ -43,16 +57,29 @@ export interface ParticipantTileProps {
avatarUrl: string | null;
me: boolean;
muted: boolean;
/** Local-only: true when THIS user has muted everyone else via the deafen
* toggle. Remote deafen state is not propagated, so only the own tile
* ever carries a truthy value. */
/** True when the participant has deafened. Propagated from peers via the
* LiveKit data channel, so remote tiles surface the headphones-off badge
* the same way the local tile does. */
deafened: boolean;
/** Discord-style: true if this participant is the call host (initiator).
* Drives the crown badge. Hidden in 1:1 calls — set false there. */
isHost?: boolean;
/** True iff the user has pinned this tile via right-click → "Anpinnen".
* Renders a small pin badge top-left and locks the focus mode to this
* tile regardless of who is currently speaking. */
pinned?: boolean;
/** Discord-style "LIVE" pill — true iff this participant is currently
* publishing a screen share. Shown on the user tile so observers know
* the share-tile next to it belongs to this person. */
streaming?: boolean;
speaking: boolean;
video: boolean;
e2ee: boolean;
/** MediaStreamTrack for the participant's active camera, when `video` is
* true. When null the tile falls back to the avatar placeholder. */
videoTrack?: MediaStreamTrack | null;
/** SFU-measured connection quality. Only `poor` and `lost` show a badge. */
connectionQuality?: TileConnectionQuality;
size?: 'default' | 'small';
focused?: boolean;
onClick?: () => void;
@@ -68,6 +95,10 @@ export function CallParticipantTile(props: ParticipantTileProps) {
speaking,
video,
e2ee,
connectionQuality,
isHost = false,
pinned = false,
streaming = false,
size = 'default',
focused = false,
onClick,
@@ -75,14 +106,13 @@ export function CallParticipantTile(props: ParticipantTileProps) {
} = props;
const small = size === 'small';
// Split the speaking indicator per-mode so we don't stack a tile border
// + inset glow on top of the avatar pulse (visual double-chrome). Video
// tiles get the border (the avatar is hidden behind the stream so the
// pulse wouldn't be visible anyway); audio tiles rely on the avatar
// pulse rendered inside AudioContent.
const videoSpeaking = speaking && video;
const borderClass = videoSpeaking
? 'border-emerald-500 shadow-[0_0_0_2px_rgba(22,163,74,0.25)] dark:border-emerald-400'
// Discord-style: full tile border switches to emerald the whole time the
// user is speaking. Same treatment for audio + video tiles so the visual
// is consistent regardless of camera state. The inner-glow span on top of
// it adds a subtle inset highlight that reads even when the underlying
// tile is bright (e.g. a video stream).
const borderClass = speaking
? 'border-emerald-500 shadow-[0_0_0_2px_rgba(22,163,74,0.35)] dark:border-emerald-400'
: focused
? 'border-accent'
: 'border-line hover:border-accent';
@@ -92,7 +122,7 @@ export function CallParticipantTile(props: ParticipantTileProps) {
onClick={onClick}
onContextMenu={onContextMenu}
className={
'relative flex flex-col overflow-hidden rounded-[14px] border bg-surface-3 transition ' +
'relative flex flex-col overflow-hidden rounded-[14px] border-[2px] bg-surface-3 transition-colors duration-150 ' +
(onClick ? 'cursor-pointer ' : '') +
borderClass +
(small ? ' min-w-[140px]' : '')
@@ -104,19 +134,55 @@ export function CallParticipantTile(props: ParticipantTileProps) {
<AudioContent {...props} small={small} />
)}
{/* Video-only speaking indicator. Audio tiles use the avatar pulse
from AudioContent so we don't double-render chrome. z-10 keeps
it above the video element. */}
{videoSpeaking && (
{speaking && (
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-10 rounded-[14px] border-[3px] border-emerald-400 shadow-[inset_0_0_18px_rgba(34,197,94,0.55)]"
className="pointer-events-none absolute inset-0 z-10 rounded-[12px] shadow-[inset_0_0_18px_rgba(34,197,94,0.45)]"
/>
)}
<div className="pointer-events-none absolute inset-x-2 bottom-2 flex items-center justify-between gap-2 rounded-lg glass-chip px-2.5 py-1.5">
<ConnectionQualityBadge quality={connectionQuality} small={small} />
{pinned && (
<span
aria-label="Angepinnt"
title="Angepinnt"
className={
'pointer-events-none absolute left-2 top-2 z-20 flex items-center justify-center rounded-md bg-accent text-accent-fg ' +
(small ? 'h-5 w-5' : 'h-6 w-6')
}
>
<PinIcon className={small ? 'h-3 w-3' : 'h-3.5 w-3.5'} />
</span>
)}
{streaming && (
<span
aria-label="Streamt gerade"
title="Streamt gerade"
className={
'pointer-events-none absolute z-20 flex items-center gap-1 rounded-md bg-rose-600 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white shadow ' +
(pinned ? 'left-9 top-2' : 'left-2 top-2')
}
>
<span className="h-1.5 w-1.5 rounded-full bg-white motion-safe:animate-live-dot" />
LIVE
</span>
)}
{/* Reserve enough vertical space so the chip's height stays constant
whether or not the muted/deafened badges are present. Without
`min-h-5` on the right slot the chip grows by ~4px the moment a
badge appears, which makes the dock visibly jiggle when somebody
mutes mid-call. */}
<div className="pointer-events-none absolute inset-x-2 bottom-2 flex min-h-[32px] items-center justify-between gap-2 rounded-lg glass-chip px-2.5 py-1.5">
<div className="flex min-w-0 items-center gap-1.5 text-xs font-semibold">
{me && <CrownIcon className="h-3 w-3 shrink-0 text-amber-300" />}
{isHost && (
<CrownIcon
aria-label="Anrufgründer"
className="h-3 w-3 shrink-0 text-amber-300"
/>
)}
<span className="truncate">
{displayName}
{me ? ' (du)' : ''}
@@ -131,7 +197,7 @@ export function CallParticipantTile(props: ParticipantTileProps) {
</span>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
<div className="flex min-h-5 shrink-0 items-center gap-1">
{muted && (
<span
aria-label="Mikro stumm"
@@ -156,11 +222,45 @@ export function CallParticipantTile(props: ParticipantTileProps) {
);
}
/** Discord-style: hidden when excellent/good/unknown, amber when poor,
* red with a struck-through Wifi when lost. Anchored top-right above the
* speaking-glow so it stays readable on a bright video stream. */
function ConnectionQualityBadge({
quality,
small,
}: {
quality: TileConnectionQuality | undefined;
small: boolean;
}) {
if (!quality || quality === 'excellent' || quality === 'good' || quality === 'unknown') {
return null;
}
const isLost = quality === 'lost';
const Icon = isLost ? WifiOffIcon : WifiLowIcon;
const label = isLost ? 'Verbindung verloren' : 'Schlechte Verbindung';
const tone = isLost
? 'bg-rose-500/85 text-white'
: 'bg-amber-400/90 text-amber-950 dark:text-amber-950';
const sz = small ? 'h-5 w-5' : 'h-6 w-6';
const iconSz = small ? 'h-3 w-3' : 'h-3.5 w-3.5';
return (
<span
aria-label={label}
title={label}
className={
'pointer-events-none absolute right-2 top-2 z-20 flex items-center justify-center rounded-md ' +
sz + ' ' + tone
}
>
<Icon className={iconSz} />
</span>
);
}
function AudioContent({
userId,
displayName,
avatarUrl,
speaking,
small,
}: ParticipantTileProps & { small: boolean }) {
const key = colorKeyFor(userId);
@@ -174,12 +274,6 @@ function AudioContent({
(small ? 'h-11 w-11' : 'h-[72px] w-[72px]')
}
>
{speaking && (
<span
aria-hidden="true"
className="absolute -inset-1.5 animate-audio-pulse rounded-full border-2 border-emerald-500 dark:border-emerald-400"
/>
)}
{avatarUrl ? (
<img
src={avatarUrl}