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,18 +1,15 @@
import type { ConversationSummary } from '@chat-app/shared/chat';
import type { PresenceState } from '@chat-app/shared/supabase';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import { useCall } from '../context/CallContext';
import { useCallPresence } from '../lib/useCallPresence';
import type { PeerPresence } from '../lib/usePeerPresence';
import { Avatar } from './Avatar';
import {
GridIcon,
InfoIcon,
PhoneIcon,
SearchIcon,
SpinnerIcon,
UsersIcon,
VideoIcon,
} from './icons';
@@ -29,12 +26,21 @@ interface Props {
conversation: ConversationSummary | null;
peerPresence: PeerPresence | null;
onInfoClick?: () => void;
onMediaClick?: () => void;
onProfileClick?: (ev: React.MouseEvent) => void;
onSearchClick?: () => void;
}
export function ConversationHeader({ conversation, peerPresence, onInfoClick, onSearchClick }: Props) {
export function ConversationHeader({
conversation,
peerPresence,
onInfoClick,
onMediaClick,
onProfileClick,
onSearchClick,
}: Props) {
if (!conversation) {
return <header className="h-[57px] border-b border-line px-6 py-3" aria-busy="true" />;
return <header className="h-[65px] border-b border-line px-6 py-3" aria-busy="true" />;
}
return (
@@ -43,9 +49,12 @@ export function ConversationHeader({ conversation, peerPresence, onInfoClick, on
conversation={conversation}
peerPresence={peerPresence}
{...(onInfoClick ? { onInfoClick } : {})}
{...(onMediaClick ? { onMediaClick } : {})}
{...(onProfileClick ? { onProfileClick } : {})}
{...(onSearchClick ? { onSearchClick } : {})}
/>
<ActiveCallBanner conversationId={conversation.id} />
{/* Voice-channel rail rendered separately in ConversationPage so it
can sit between the message surface and the in-call dock. */}
</>
);
}
@@ -54,16 +63,27 @@ interface HeaderBarProps {
conversation: ConversationSummary;
peerPresence: PeerPresence | null;
onInfoClick?: () => void;
onMediaClick?: () => void;
onProfileClick?: (ev: React.MouseEvent) => void;
onSearchClick?: () => void;
}
function HeaderBar({ conversation, peerPresence, onInfoClick, onSearchClick }: HeaderBarProps) {
function HeaderBar({
conversation,
peerPresence,
onInfoClick,
onMediaClick,
onProfileClick,
onSearchClick,
}: HeaderBarProps) {
const { t } = useTranslation(['app']);
const isDm = conversation.type === 'dm';
const title = isDm ? (conversation.peer?.displayName ?? '?') : (conversation.name ?? '?');
const handle = isDm ? '@' + (conversation.peer?.username ?? '?') : '';
const peerAvatar = isDm ? (conversation.peer?.avatarUrl ?? null) : (conversation.avatarUrl ?? null);
const peerAvatar = isDm
? (conversation.peer?.avatarUrl ?? null)
: (conversation.avatarUrl ?? null);
// Hide presence when peer chose invisible — reciprocal privacy.
const peerState = peerPresence?.state ?? null;
@@ -80,8 +100,16 @@ function HeaderBar({ conversation, peerPresence, onInfoClick, onSearchClick }: H
})();
return (
<header className="flex items-center gap-3 border-b border-line bg-surface-3 px-6 py-3">
<div className="relative">
<header className="discord-chat-surface flex min-h-[65px] items-center gap-3 border-b border-line bg-surface-3 px-5 py-3">
<button
type="button"
data-user-popover-trigger={isDm ? true : undefined}
onClick={isDm ? onProfileClick : undefined}
disabled={!isDm || !onProfileClick}
aria-label={isDm ? 'Profil öffnen' : title}
title={isDm ? 'Profil öffnen' : title}
className="relative shrink-0 rounded-full text-left transition enabled:cursor-pointer enabled:hover:ring-2 enabled:hover:ring-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-default"
>
{isDm ? (
<Avatar url={peerAvatar} displayName={title} className="h-10 w-10 text-base" />
) : peerAvatar ? (
@@ -100,9 +128,12 @@ function HeaderBar({ conversation, peerPresence, onInfoClick, onSearchClick }: H
}
/>
)}
</div>
</button>
<div className="min-w-0 flex-1">
<p className="truncate font-display text-base font-semibold text-fg">{title}</p>
<div className="flex min-w-0 items-center gap-2">
{!isDm && <UsersIcon className="h-4 w-4 shrink-0 text-fg-muted" />}
<p className="truncate font-display text-base font-semibold text-fg">{title}</p>
</div>
<p className="truncate text-xs text-fg-muted">
{isDm ? (
<>
@@ -127,6 +158,11 @@ function HeaderBar({ conversation, peerPresence, onInfoClick, onSearchClick }: H
icon={SearchIcon}
{...(onSearchClick ? { onClick: onSearchClick } : {})}
/>
<HeaderActionButton
label="Medien & Dateien"
icon={GridIcon}
{...(onMediaClick ? { onClick: onMediaClick } : {})}
/>
<CallHeaderButton conversationId={conversation.id} kind="audio" />
<CallHeaderButton conversationId={conversation.id} kind="video" />
{!isDm && onInfoClick && (
@@ -158,7 +194,7 @@ function HeaderActionButton({
const toneClass =
tone === 'accent'
? 'text-accent hover:bg-accent/10'
: 'text-fg-muted hover:bg-surface-2 hover:text-fg';
: 'text-fg-muted hover:bg-surface-2 hover:text-fg dark:hover:bg-[#383a40]';
return (
<button
type="button"
@@ -201,62 +237,3 @@ function CallHeaderButton({
);
}
function ActiveCallBanner({ conversationId }: { conversationId: string }) {
const { t } = useTranslation(['app']);
const { session } = useAuth();
const { state, joinActiveCall, lastCallConversationId, dismissLastCall } = useCall();
const active = useCallPresence(conversationId);
const myId = session?.user.id;
const iAmIn =
(state.kind === 'connected' ||
state.kind === 'connecting' ||
state.kind === 'outgoing') &&
state.conversationId === conversationId;
const othersIn = active.filter((u) => u !== myId);
// Fallback signal: I just left this conv with peers still inside. Covers the
// brief window after hangup where presence may not have re-synced yet (the
// realtime channel can churn while ConversationHeader remounts).
const justLeft = state.kind === 'idle' && lastCallConversationId === conversationId;
// Once presence confirms the room is empty, drop the "just left" hint so the
// banner hides cleanly instead of sticking forever. Grace window handles the
// brief gap between hangup and presence re-sync so we don't flicker.
useEffect(() => {
if (!justLeft) return;
if (othersIn.length > 0) return; // still live — keep banner
const id = window.setTimeout(() => dismissLastCall(), 3000);
return () => window.clearTimeout(id);
}, [justLeft, othersIn.length, dismissLastCall]);
if (iAmIn) return null;
if (othersIn.length === 0 && !justLeft) return null;
const count = Math.max(othersIn.length, justLeft ? 1 : 0);
const busy = state.kind !== 'idle';
return (
<div className="flex items-center gap-3 border-b border-emerald-500/30 bg-emerald-500/10 px-6 py-2.5 text-sm text-emerald-700 dark:text-emerald-100">
<PhoneIcon className="h-4 w-4 text-emerald-600 dark:text-emerald-300" />
<span className="flex-1">
{t('app:call.active_in_conv', {
defaultValue: 'Active call · {{count}} in room',
count,
})}
</span>
<button
type="button"
onClick={() => void joinActiveCall(conversationId, 'audio')}
disabled={busy}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-emerald-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-400/40 disabled:cursor-not-allowed disabled:opacity-60"
>
{busy ? (
<SpinnerIcon className="h-3.5 w-3.5" />
) : (
<PhoneIcon className="h-3.5 w-3.5" />
)}
<span>{t('app:call.join', { defaultValue: 'Join' })}</span>
</button>
</div>
);
}