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,234 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
ConversationAttachmentIndex,
|
||||
ConversationAttachmentItem,
|
||||
} from '../lib/conversationFeatures';
|
||||
import { AttachmentGeneric } from './AttachmentGeneric';
|
||||
import { AttachmentImage } from './AttachmentImage';
|
||||
import { ArrowRightIcon, FileIcon, ImageIcon, MicIcon, VideoIcon, XIcon } from './icons';
|
||||
|
||||
type DrawerTab = 'media' | 'files' | 'audio';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
index: ConversationAttachmentIndex;
|
||||
senderNameFor: (senderId: string) => string;
|
||||
onJumpToMessage: (messageId: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TAB_LABELS: Record<DrawerTab, string> = {
|
||||
media: 'Medien',
|
||||
files: 'Dateien',
|
||||
audio: 'Audio',
|
||||
};
|
||||
|
||||
export function MediaFilesDrawer({ open, index, senderNameFor, onJumpToMessage, onClose }: Props) {
|
||||
const [tab, setTab] = useState<DrawerTab>('media');
|
||||
const items = index[tab];
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<aside className="absolute inset-y-0 right-0 z-40 flex w-full max-w-[380px] flex-col border-l border-line bg-surface-2 shadow-2xl dark:bg-[#2b2d31]">
|
||||
<header className="flex min-h-[65px] items-center gap-3 border-b border-line px-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent/10 text-accent">
|
||||
<ImageIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-display text-base font-semibold text-fg">Medien & Dateien</p>
|
||||
<p className="text-xs text-fg-muted">{index.all.length} Elemente im geladenen Verlauf</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Drawer schließen"
|
||||
title="Drawer schließen"
|
||||
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-surface-3 hover:text-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:hover:bg-[#383a40]"
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-3 gap-1 border-b border-line p-2">
|
||||
{(['media', 'files', 'audio'] as DrawerTab[]).map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => setTab(item)}
|
||||
className={
|
||||
'cursor-pointer rounded-lg px-2 py-2 text-xs font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
|
||||
(tab === item
|
||||
? 'bg-accent text-accent-fg'
|
||||
: 'text-fg-muted hover:bg-surface-3 hover:text-fg dark:hover:bg-[#383a40]')
|
||||
}
|
||||
>
|
||||
{TAB_LABELS[item]} · {index[item].length}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||
{items.length === 0 ? (
|
||||
<EmptyState tab={tab} />
|
||||
) : tab === 'media' ? (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{items.map((item) => (
|
||||
<MediaTile
|
||||
key={item.handle.id}
|
||||
item={item}
|
||||
senderName={senderNameFor(item.senderId)}
|
||||
onJumpToMessage={onJumpToMessage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<FileRow
|
||||
key={item.handle.id}
|
||||
item={item}
|
||||
senderName={senderNameFor(item.senderId)}
|
||||
onJumpToMessage={onJumpToMessage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaTile({
|
||||
item,
|
||||
senderName,
|
||||
onJumpToMessage,
|
||||
}: {
|
||||
item: ConversationAttachmentItem;
|
||||
senderName: string;
|
||||
onJumpToMessage: (messageId: string) => void;
|
||||
}) {
|
||||
const isImage = item.handle.mimeType.startsWith('image/');
|
||||
const isVideo = item.handle.mimeType.startsWith('video/');
|
||||
return (
|
||||
<article className="overflow-hidden rounded-lg border border-line bg-surface-3 dark:bg-[#313338]">
|
||||
<div className="flex aspect-square items-center justify-center overflow-hidden bg-black/20">
|
||||
{isImage ? (
|
||||
<div className="flex max-h-full max-w-full items-center justify-center p-1 [&_button]:m-0 [&_img]:max-h-36">
|
||||
<AttachmentImage handle={item.handle} />
|
||||
</div>
|
||||
) : isVideo ? (
|
||||
<div className="flex flex-col items-center gap-2 text-fg-muted">
|
||||
<VideoIcon className="h-7 w-7" />
|
||||
<span className="text-xs">Video</span>
|
||||
</div>
|
||||
) : (
|
||||
<ImageIcon className="h-7 w-7 text-fg-muted" />
|
||||
)}
|
||||
</div>
|
||||
<AttachmentMeta item={item} senderName={senderName} onJumpToMessage={onJumpToMessage} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function FileRow({
|
||||
item,
|
||||
senderName,
|
||||
onJumpToMessage,
|
||||
}: {
|
||||
item: ConversationAttachmentItem;
|
||||
senderName: string;
|
||||
onJumpToMessage: (messageId: string) => void;
|
||||
}) {
|
||||
const isAudio = item.handle.mimeType.startsWith('audio/');
|
||||
return (
|
||||
<article className="rounded-lg border border-line bg-surface-3 p-2 dark:bg-[#313338]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent">
|
||||
{isAudio ? <MicIcon className="h-5 w-5" /> : <FileIcon className="h-5 w-5" />}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold text-fg">{labelFor(item.handle.mimeType)}</p>
|
||||
<p className="truncate text-xs text-fg-muted">
|
||||
{formatSize(item.handle.sizeBytes)} · {senderName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isAudio && <AttachmentGeneric handle={item.handle} />}
|
||||
<AttachmentMeta
|
||||
item={item}
|
||||
senderName={senderName}
|
||||
onJumpToMessage={onJumpToMessage}
|
||||
compact
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentMeta({
|
||||
item,
|
||||
senderName,
|
||||
onJumpToMessage,
|
||||
compact = false,
|
||||
}: {
|
||||
item: ConversationAttachmentItem;
|
||||
senderName: string;
|
||||
onJumpToMessage: (messageId: string) => void;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const date = useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(item.createdAt)),
|
||||
[item.createdAt],
|
||||
);
|
||||
return (
|
||||
<div className={(compact ? 'pt-2' : 'p-2') + ' flex items-center gap-2'}>
|
||||
<div className="min-w-0 flex-1">
|
||||
{!compact && <p className="truncate text-xs font-semibold text-fg">{senderName}</p>}
|
||||
<p className="truncate text-[11px] text-fg-muted">{date}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onJumpToMessage(item.messageId)}
|
||||
aria-label="Zur Nachricht springen"
|
||||
title="Zur Nachricht springen"
|
||||
className="flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-surface-2 hover:text-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:hover:bg-[#383a40]"
|
||||
>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ tab }: { tab: DrawerTab }) {
|
||||
const Icon = tab === 'media' ? ImageIcon : tab === 'audio' ? MicIcon : FileIcon;
|
||||
return (
|
||||
<div className="flex h-full min-h-[280px] flex-col items-center justify-center gap-3 text-center text-fg-muted">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-xl bg-surface-3 dark:bg-[#313338]">
|
||||
<Icon className="h-6 w-6" />
|
||||
</span>
|
||||
<p className="max-w-48 text-sm">
|
||||
Noch keine {TAB_LABELS[tab].toLowerCase()} im geladenen Verlauf.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function labelFor(mimeType: string): string {
|
||||
if (mimeType.startsWith('audio/')) return 'Audiodatei';
|
||||
if (mimeType === 'application/pdf') return 'PDF-Datei';
|
||||
if (mimeType.startsWith('text/')) return 'Textdatei';
|
||||
return mimeType || 'Datei';
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB';
|
||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
Reference in New Issue
Block a user